Путь заявки из всех источников: сделка, QUIK, события DEPO (с обработкой/ошибкой), поручение 16 и документы НРД (регистрация, GS036, GS116 сквитовано, MS010); «Обновлена» — последнее событие; статус учитывает сквитовку (GS116), время MS010 в строке статуса

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-09-29 12:44:52 +03:00
parent e123eda691
commit 340fca7bd7
7 changed files with 405 additions and 46 deletions
+4 -40
View File
@@ -627,6 +627,9 @@ func (a *admin) renderClaim(w http.ResponseWriter, r *http.Request, svc *Service
return
}
view.Timeline = buildClaimTimeline(r.Context(), view)
if last := timelineLast(view.Timeline); last.After(view.UpdatedAt) {
view.UpdatedAt = last // «Обновлена» — последнее событие по заявке, а не последняя смена состояния сделки
}
var depo *OutgoingDeal
if view.M2MGUID != "" && globalOutgoingStore != nil {
if d, ok := scanForm16Deal(globalOutgoingStore, string(view.M2MGUID)); ok {
@@ -689,46 +692,7 @@ func buildClaimTimeline(ctx context.Context, v ClaimView) []TimelineEvent {
}
}
// Происхождение из ЛК ЕСИА Финанс (путь напрямую через коннектор, минуя
// Fancy): показываем стартовый шаг с номером заявки в ЛК.
if v.M2MGUID != "" {
if lkID, ok := LKClaimIDByGUID(string(v.M2MGUID)); ok {
add("Заявка из ЛК ЕСИА Финанс", "db",
fmt.Sprintf("Забрана коннектором из личного кабинета (заявка ЛК #%d).", lkID),
v.CreatedAt, time.Time{})
}
}
// Отправлена в шлюз НРД (Deal создаётся в момент отправки).
add("Отправлена в интеграционный шлюз НРД", "gateway", "Подписана ключом и передана в ИШ.", v.CreatedAt, time.Time{})
// Ответ НРД (если получен).
if v.M2MDecision != nil {
conf, rej := 0, 0
for _, s := range v.M2MDecision.Securities {
if s.Outcome == "confirmed" {
conf++
} else {
rej++
}
}
title := "Получено решение контрагента (НРД)"
if devMode(globalRC) {
title = "Получено решение НРД (робот)"
}
add(title, "response",
fmt.Sprintf("Принято %d, отклонено %d.", conf, rej), v.UpdatedAt, v.CreatedAt)
} else if v.M2MResponse != nil {
var codes string
for _, e := range v.M2MResponse.Responses {
if e.Code != "" {
codes += e.Code + " "
}
}
add("Ответ сервиса МОСТ (НРД)", "response",
"Статус: "+v.M2MResponse.StatusCode+". "+strings.TrimSpace(codes), v.UpdatedAt, v.CreatedAt)
}
return tl
return claimTimelineFull(ctx, v)
}
// formatSince — человекочитаемая длительность между шагами таймлайна.
+276
View File
@@ -0,0 +1,276 @@
package lkgateway
// Путь заявки (исходящий перевод) во времени — из всех источников, а не только из состояний сделки:
// сделка M2M (создание, отправка, решение контрагента), блокировка/подпись в QUIK МНП, события обмена
// с DEPO (FTV_M2M_EVENTS: время, обработано/ошибка), поручение 16 и все документы НРД по нему
// (регистрация, «нет встречного поручения» GS036, исполнение MS010), недоставленные отправки.
// Шаги сортируются по времени, между соседними — длительность. Последним — чего ждём сейчас.
import (
"context"
"database/sql"
"fmt"
"sort"
"strings"
"time"
)
var mskZone = time.FixedZone("МСК", 3*3600)
type tlItem struct {
at time.Time
title, kind, detail string
}
// depoEventRow — строка FTV_M2M_EVENTS по переводу.
type depoEventRow struct {
At time.Time
Type string
Ref string
State string
Comment string
}
// depoEventsFor — события обмена с DEPO по переводу (EVENT_DATE пишется временем МСК без зоны).
func depoEventsFor(ctx context.Context, rc *RuntimeConfig, guid string) []depoEventRow {
if rc == nil || guid == "" {
return nil
}
db, err := depoEventsDB(rc)
if err != nil {
return nil
}
defer db.Close()
cctx, cancel := context.WithTimeout(ctx, 4*time.Second)
defer cancel()
rows, err := db.QueryContext(cctx, `SELECT EVENT_TYPE, EVENT_DATE, ISNULL(REFERENCE,''), ISNULL(STATE,''), ISNULL(COMMENT,'')
FROM dbo.FTV_M2M_EVENTS WHERE GUID=@g ORDER BY EVENT_DATE`, sql.Named("g", strings.ToUpper(guid)))
if err != nil {
return nil
}
defer rows.Close()
var out []depoEventRow
for rows.Next() {
var e depoEventRow
var at time.Time
if rows.Scan(&e.Type, &at, &e.Ref, &e.State, &e.Comment) != nil {
continue
}
// datetime без зоны: драйвер отдаёт стенные часы в UTC — это время МСК
e.At = time.Date(at.Year(), at.Month(), at.Day(), at.Hour(), at.Minute(), at.Second(), at.Nanosecond(), mskZone)
out = append(out, e)
}
return out
}
var depoEventTitles = map[string]string{
"BJ_SENT": "DEPO: перевод отправлен в НРД",
"BJ_GATEWAY_REJECT": "DEPO: заявка отклонена шлюзом bj",
"BJ_SERVICE_ERROR": "DEPO: сервис M2M отверг перевод",
"BJ_CP_CONFIRM": "DEPO: контрагент согласился",
"BJ_CP_REJECT": "DEPO: контрагент отказал",
"BJ_ORDER_SENT": "DEPO: поручение 16 подано в НРД",
"BJ_ORDER_LATE": "DEPO: поручение не исполнено за 5 минут",
"BJ_ORDER_ACCEPTED": "DEPO: НРД исполнил поручение (MS010)",
"BJ_ORDER_REJECTED": "DEPO: НРД отверг поручение (MS010)",
"BJ_EXECUTED": "DEPO: перевод исполнен",
"BJ_CANCELLED": "DEPO: поручение отменено (срок 30 дней)",
"BJ_QUIK_BLOCKED": "DEPO: бумаги заблокированы в QUIK",
"BJ_QUIK_SIGNED": "DEPO: поручение подписано в QUIK",
"BJ_QUIK_RELEASED": "DEPO: блокировка в QUIK снята",
"BJ_QUIK_DEPOSITED": "DEPO: бумаги зачислены в QUIK",
"BJ_RECEIVED": "DEPO: входящий перевод получен",
"BJ_DECISION_CONFIRM": "DEPO: наше согласие отправлено",
"BJ_DECISION_REJECT": "DEPO: наш отказ отправлен",
}
func depoEventDetail(e depoEventRow) (string, string) {
kind := "db"
var parts []string
if e.Ref != "" {
parts = append(parts, "строка "+e.Ref)
}
switch strings.ToUpper(e.State) {
case "ERROR":
kind = "reject"
c := e.Comment
if i := strings.Index(c, "//"); i > 0 { // служебный хвост обработчика DEPO
c = c[:i]
}
parts = append(parts, "ошибка обработки в DEPO: "+strings.TrimSpace(c))
case "DONE", "OK", "SUCCESS":
parts = append(parts, "обработано DEPO")
case "", "TODO", "NEW":
parts = append(parts, "ждёт обработки DEPO")
default:
parts = append(parts, "DEPO: "+e.State)
}
return strings.Join(parts, " · "), kind
}
var quikActionTitles = map[string]string{
"register": "QUIK МНП: вывод зарегистрирован, бумаги заблокированы",
"sign": "QUIK МНП: поручение подписано, бумаги списаны",
"deny": "QUIK МНП: поручение снято, блокировка снята",
"deposit": "QUIK МНП: бумаги зачислены",
"recipient": "QUIK МНП: получатель указан",
}
func claimTimelineFull(ctx context.Context, v ClaimView) []TimelineEvent {
guid := string(v.M2MGUID)
var items []tlItem
add := func(at time.Time, title, kind, detail string) {
items = append(items, tlItem{at: at, title: title, kind: kind, detail: detail})
}
// создание и откуда пришла
origin := "Перевод M2M создан в bj."
if guid != "" {
if lkID, ok := LKClaimIDByGUID(guid); ok {
origin = fmt.Sprintf("Заявка из личного кабинета ЕСИА Финанс (заявка ЛК #%d).", lkID)
}
}
add(v.CreatedAt, "Заявка создана", "db", origin)
// состояния сделки M2M
decided := false
for _, st := range v.Stages {
switch st.State {
case "submitted_to_nsd":
add(st.EnteredAt, "Запрос перевода отправлен в НРД", "gateway", "M2MTransferRequest подписан шлюзом, НРД принял пакет.")
case "manual_approval":
add(st.EnteredAt, "Передана на решение оператора", "warn", st.Reason)
case "confirmed", "rejected":
decided = true
title, kind := "Контрагент согласился на перевод", "response"
if st.State == "rejected" {
title, kind = "Перевод отклонён", "reject"
}
detail := st.Reason
if v.M2MDecision != nil {
conf, rej := 0, 0
for _, s := range v.M2MDecision.Securities {
if s.Outcome == "confirmed" {
conf++
} else {
rej++
}
}
detail = fmt.Sprintf("Решение контрагента (M2MTransferDecision): принято бумаг %d, отклонено %d.", conf, rej)
} else if v.M2MResponse != nil {
var codes []string
for _, e := range v.M2MResponse.Responses {
if e.Code != "" {
codes = append(codes, e.Code)
}
}
detail = "Ответ сервиса M2M: " + v.M2MResponse.StatusCode + " " + strings.Join(codes, " ")
}
add(st.EnteredAt, title, kind, strings.TrimSpace(detail))
case "timed_out":
add(st.EnteredAt, "Истёк срок ответа контрагента", "reject", st.Reason)
case "done":
add(st.EnteredAt, "Перевод завершён", "ok", "")
}
}
if !decided && v.M2MResponse != nil { // ответ сервиса M2M без смены состояния
add(v.UpdatedAt, "Ответ сервиса M2M (НРД)", "response", "Статус: "+v.M2MResponse.StatusCode)
}
if guid == "" {
return timelineFinish(items, v)
}
// недоставленная отправка
if f := sendFailGet(guid); f != nil {
add(f.At, "НРД не принял пакет", "reject", fmt.Sprintf("%s. Попыток: %d; повтор — автоматически, когда НРД ответит.", sendFailText(f), f.Attempts))
}
// QUIK МНП
for _, l := range globalQuikLinks.ForGUID(guid) {
for _, st := range l.Steps {
title := quikActionTitles[st.Action]
if title == "" {
title = "QUIK МНП: " + st.Action
}
kind := "quik"
if !st.OK {
kind = "reject"
}
add(st.At, title, kind, fmt.Sprintf("%s × %v. %s", l.ISIN, l.Qty, st.Reply))
}
}
// DEPO: номер поручения и события обмена
if dl := globalDepoLinks.Get(guid); dl != nil && dl.DocNo != "" {
add(dl.DocAt, "DEPO создала поручение №"+dl.DocNo, "db", "Поручение в учёте депозитария.")
}
for _, e := range depoEventsFor(ctx, globalRC, guid) {
title := depoEventTitles[e.Type]
if title == "" {
title = "DEPO: " + e.Type
}
detail, kind := depoEventDetail(e)
add(e.At, title, kind, detail)
}
// поручение 16 и документы НРД по нему
if globalOutgoingStore != nil {
if od, ok := scanForm16Deal(globalOutgoingStore, guid); ok {
if !od.DraftAt.IsZero() {
add(od.DraftAt, "НРД прислал черновик поручения 16", "response", fmt.Sprintf("Черновик MF010 по %d бумагам.", len(od.Secs)))
}
if od.Submitted {
add(od.OrderAt, "Поручение 16 подано в НРД", "gateway", "Пакеты ИШ "+od.Pkgs+", номера поручений "+strings.Join(od.ContrDIDs, ", ")+".")
for _, d := range nsdOrderDocs(append(append([]string{}, od.ContrDIDs...), orderWatchRefs(guid)...)...) {
kind := "response"
switch {
case d.Rejected || d.Code == "REG_REJECT" || d.Code == "ERROR":
kind = "reject"
case d.Code == "GS036":
kind = "warn"
case d.Done:
kind = "ok"
}
add(d.Time, d.Title, kind, d.About)
}
}
}
}
if w := globalOrderWatch.Get(guid); w != nil && !w.LateAt.IsZero() {
add(w.LateAt, "Нет отчёта MS010 за 5 минут", "warn", "Регламентный срок исполнения прошёл; ожидаем исполнения до "+w.OrderAt.Add(orderExecWithin).In(mskZone).Format("02.01.2006")+".")
}
return timelineFinish(items, v)
}
// timelineFinish — сортировка по времени, длительности между шагами и «чего ждём» последним.
func timelineFinish(items []tlItem, v ClaimView) []TimelineEvent {
sort.SliceStable(items, func(i, j int) bool { return items[i].at.Before(items[j].at) })
var out []TimelineEvent
var prev time.Time
for _, it := range items {
if it.at.IsZero() {
continue
}
ev := TimelineEvent{Title: it.title, Kind: it.kind, Detail: it.detail, Time: it.at.In(mskZone)}
if !prev.IsZero() && it.at.After(prev) {
ev.Since = formatSince(it.at.Sub(prev))
}
out = append(out, ev)
prev = it.at
}
if l := liveOutgoingState(v); l.Active {
out = append(out, TimelineEvent{Title: "Сейчас: " + l.Text, Kind: "wait"})
}
return out
}
// timelineLast — время последнего шага (для «Обновлена»).
func timelineLast(tl []TimelineEvent) time.Time {
var last time.Time
for _, e := range tl {
if e.Time.After(last) {
last = e.Time
}
}
return last
}
+3
View File
@@ -140,6 +140,9 @@ func liveOutgoingState(c ClaimView) LiveStatus {
case depo.Done && depo.Rejected:
return LiveStatus{Text: "поручение 16 завершено отказом НРД (MS010)", Kind: "bad"}
case depo.Done:
if watch != nil && !watch.MS010At.IsZero() {
return LiveStatus{Text: "поручение 16 исполнено: MS010 получен " + watch.MS010At.In(mskZone).Format("02.01.2006 15:04"), Kind: "ok"}
}
return LiveStatus{Text: "поручение 16 исполнено: MS010 получен", Kind: "ok"}
case watch != nil:
k := watch.Kind()
+11 -2
View File
@@ -39,7 +39,16 @@ func TestNoCounterNotice(t *testing.T) {
t.Fatalf("входящий: %q", s)
}
w.Direction, w.MS010At = "out", at.Add(53*time.Minute)
if s := w.StatusRu(); !strings.Contains(s, "исполнено") || !strings.Contains(s, "задержка на стороне контрагента") {
t.Fatalf("исполнено с задержкой: %q", s)
if s := w.StatusRu(); !strings.Contains(s, "исполнено") || !strings.Contains(s, "подано не сразу") {
t.Fatalf("исполнено, встречное не сразу: %q", s)
}
// сквитовано через минуту, исполнение НРД через 52 мин — задержка у НРД, не у контрагента
w.MatchedAt = at.Add(time.Minute)
if s := w.StatusRu(); strings.Contains(s, "не сразу") || !strings.Contains(s, "сквитовано со встречным в 17:51, исполнение НРД — через 52 мин") {
t.Fatalf("исполнено после сквитовки: %q", s)
}
w.MS010At = time.Time{}
if s := w.StatusRu(); !strings.Contains(s, "сквитовано со встречным") || !strings.Contains(s, "ждём исполнения НРД") {
t.Fatalf("сквитовано, ждём: %q", s)
}
}
+57 -4
View File
@@ -38,6 +38,8 @@ type OrderWatch struct {
// NoCounterAt — НРД прислал уведомление GS036 «об отсутствии встречного поручения»: контрагент
// ещё не подал своё поручение (к нашей 16 — его 16/1, к нашей 16/1 — его 16). Сбрасывается MS010.
NoCounterAt time.Time `json:"no_counter_at,omitempty"`
// MatchedAt — НРД сквитовал поручения со встречными (GS116, MACH) по всем строкам: дальше — исполнение.
MatchedAt time.Time `json:"matched_at,omitempty"`
// Lines — состояние по каждой строке пакета (одно депо-поручение = одна бумага),
// ключ — contr_d_id. По договорённости с Fancy (18.09.2026) исходы пишутся в DEPO
// по строкам с REFERENCE и сразу по MS010 этой строки, не дожидаясь остальных:
@@ -53,6 +55,7 @@ type OrderLineWatch struct {
Rejected bool `json:"rejected,omitempty"`
ExpiredAt time.Time `json:"expired_at,omitempty"`
NoCounterAt time.Time `json:"no_counter_at,omitempty"` // GS036 по этой строке
MatchedAt time.Time `json:"matched_at,omitempty"` // GS116 MACH по этой строке
}
// counterName — чьё встречное поручение ждём: к нашей 16 — 16/1 принимающего, к нашей 16/1 — 16 передающего.
@@ -120,14 +123,19 @@ func (w *OrderWatch) StatusRu() string {
case !w.MS010At.IsZero():
late := ""
if w.MS010At.Sub(w.OrderAt) > orderMS010Within {
late = fmt.Sprintf(", с опозданием %s", w.MS010At.Sub(w.OrderAt).Round(time.Minute))
late = ", с опозданием " + humanDur(w.MS010At.Sub(w.OrderAt))
}
if !w.NoCounterAt.IsZero() {
late += "; задержка на стороне контрагента — встречное " + w.counterName() + " подано не сразу (уведомление НРД " + w.NoCounterAt.Local().Format("15:04") + ")"
switch {
case !w.MatchedAt.IsZero() && w.MS010At.Sub(w.MatchedAt) > orderMS010Within:
late += "; сквитовано со встречным в " + w.MatchedAt.Local().Format("15:04") + ", исполнение НРД — через " + humanDur(w.MS010At.Sub(w.MatchedAt))
case w.MatchedAt.IsZero() && !w.NoCounterAt.IsZero():
late += "; встречное " + w.counterName() + " подано не сразу (уведомление НРД " + w.NoCounterAt.Local().Format("15:04") + ")"
}
return "исполнено: MS010 получен " + w.MS010At.Local().Format("02.01.2006 15:04") + late
case !w.ExpiredAt.IsZero():
return "СРОК 30 ДНЕЙ ИСТЁК " + w.ExpiredAt.Local().Format("02.01.2006") + " — поручение считается отменённым"
case !w.MatchedAt.IsZero():
return "поручение сквитовано со встречным (НРД " + w.MatchedAt.Local().Format("02.01 15:04") + "), ждём исполнения НРД (MS010)"
case !w.NoCounterAt.IsZero():
return "НРД: контрагент ещё не подал встречное " + w.counterName() + " (уведомление НРД " + w.NoCounterAt.Local().Format("02.01 15:04") + "); наше поручение принято, ждём исполнения"
case !w.LateAt.IsZero():
@@ -341,7 +349,9 @@ func orderWatchCheck(ctx context.Context, rc *RuntimeConfig, guid, direction str
}
if fresh {
why := "отчёт MS010 не получен"
if !w.NoCounterAt.IsZero() {
if !w.MatchedAt.IsZero() {
why = "поручения сквитованы со встречными (" + w.MatchedAt.Local().Format("15:04") + "), ждём исполнения НРД"
} else if !w.NoCounterAt.IsZero() {
why = "НРД сообщил, что контрагент ещё не подал встречное " + w.counterName() + " — наше поручение принято, исполнение ждёт контрагента"
}
raiseSystemAlert(rc, "НРД: депо-поручение не исполнено в срок 5 минут",
@@ -489,6 +499,36 @@ func orderWatchRefs(guid string) []string {
func orderNoteNoCounter(guid string, w *OrderWatch, lines []orderLine) {
for _, ln := range lines {
st := w.Lines[ln.ContrDID]
// отчёт GS116 «сквитовано» (MACH) — по номеру поручения
if st == nil || st.MatchedAt.IsZero() {
if at, ok := findMatchedNotice(ln.ContrDID); ok {
globalOrderWatch.update(guid, func(w *OrderWatch) {
if w.Lines == nil {
w.Lines = map[string]*OrderLineWatch{}
}
l := w.Lines[ln.ContrDID]
if l == nil {
l = &OrderLineWatch{Ref: ln.Ref, ISIN: ln.ISIN}
w.Lines[ln.ContrDID] = l
}
l.MatchedAt = at
all, last := true, time.Time{}
for _, x := range lines {
lx := w.Lines[x.ContrDID]
if lx == nil || lx.MatchedAt.IsZero() {
all = false
break
}
if lx.MatchedAt.After(last) {
last = lx.MatchedAt
}
}
if all {
w.MatchedAt = last
}
})
}
}
// уведомление «нет встречного поручения» (GS036) — по REFERENCE строки
if (st == nil || st.NoCounterAt.IsZero()) && ln.Ref != "" {
if at, ok := findNoCounterNotice(ln.Ref); ok {
@@ -511,3 +551,16 @@ func orderNoteNoCounter(guid string, w *OrderWatch, lines []orderLine) {
}
}
}
// findMatchedNotice — отчёт НРД GS116 «сквитовано со встречным» (<Mtchd>) по номеру поручения.
func findMatchedNotice(contrDID string) (time.Time, bool) {
if contrDID == "" {
return time.Time{}, false
}
for _, pk := range pkgIndex() {
if strings.Contains(pk.body, "<Mtchd>") && strings.Contains(pk.body, "GS116") && strings.Contains(pk.body, ">"+contrDID+"<") {
return pk.mt, true
}
}
return time.Time{}, false
}
+53
View File
@@ -0,0 +1,53 @@
//go:build live
package lkgateway
import (
"bytes"
"context"
"fmt"
"os"
"regexp"
"strings"
"testing"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2mcore"
)
// Путь заявки на живых данных: go test -c -tags live; запуск от bj с TL_ID=<id сделки>.
func TestTimelineLive(t *testing.T) {
rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json")
if err != nil {
t.Fatal(err)
}
globalRC = rc
globalOutgoingStore = NewOutgoingStore()
repo, err := m2mcore.NewPostgresRepository(context.Background(), rc.Snapshot().Postgres.DSN)
if err != nil {
t.Fatal(err)
}
svc := NewService(Config{Repository: repo})
for _, id := range strings.Split(os.Getenv("TL_ID"), ",") {
v, err := svc.GetClaim(context.Background(), id)
if err != nil {
t.Fatal(err)
}
v.Timeline = buildClaimTimeline(context.Background(), v)
fmt.Printf("=== %s создана %s обновлена(сделка) %s, последнее событие %s\n", id[:8], v.CreatedAt.Format("15:04:05 MST"), v.UpdatedAt.Format("15:04:05 MST"), timelineLast(v.Timeline).Format("15:04:05"))
for _, e := range v.Timeline {
fmt.Printf(" %-9s %-8s [%s] %s — %s\n", e.Time.Format("15:04:05"), e.Since, e.Kind, e.Title, e.Detail)
}
if os.Getenv("TL_HTML") != "" {
a, _ := newAdmin()
var b bytes.Buffer
if err := a.claim.ExecuteTemplate(&b, "layout", claimData{page: page{Title: "Заявка"}, Claim: v, Watch: globalOrderWatch.Get(string(v.M2MGUID))}); err != nil {
t.Fatal(err)
}
txt := regexp.MustCompile(`(?s)<style.*?</style>|<script.*?</script>`).ReplaceAllString(b.String(), "")
txt = regexp.MustCompile(`<[^>]+>`).ReplaceAllString(txt, " ")
txt = regexp.MustCompile(`\s+`).ReplaceAllString(txt, " ")
i := strings.Index(txt, "Заявка ")
fmt.Println(txt[i:min(len(txt), i+1500)])
}
}
}
@@ -22,6 +22,7 @@
border-left:5px solid transparent; border-right:5px solid transparent; border-top:7px solid var(--c) }
.tl-arrow .dt { font-size:10.5px; color:var(--muted,#999) }
.tl-db{--c:#2563eb} .tl-crane{--c:#16a34a} .tl-gateway{--c:#7c3aed} .tl-response{--c:#0891b2} .tl-reject{--c:#dc2626}
.tl-quik{--c:#b45309} .tl-ok{--c:#16a34a} .tl-warn{--c:#d97706} .tl-wait{--c:#94a3b8}
/* нет места справа (узкие/средние экраны) — таймлайн в потоке под контентом */
@media (max-width:1640px){ .claim-tl{ position:static; width:auto; max-height:none; margin-top:16px; box-shadow:none } }
</style>