package mynalogsync import ( "context" "fmt" "sync" "time" "go.uber.org/zap" "scrabble/backend/internal/payments" ) // WatchdogInterval is how often the filing deadline is checked. It reads only local tables, so the // cost is one query a day. const WatchdogInterval = 24 * time.Hour // Days of the month at which unfiled income of the previous month escalates. // // The professional-income statute gives until the 9th of the following month only for settlements // that do NOT involve an electronic means of payment; a card payment is one, so the receipt is // strictly due at the moment of settlement. The automatic export satisfies both readings, and this // watchdog is the backstop for when it is switched off or has stopped — which is exactly when the // deadline stops taking care of itself. const ( warnDay = 1 alarmDay = 5 overdueDay = 9 ) // Watchdog warns when income from a closed month is still not registered with the tax service. // // It runs whether or not the automatic export does, because the case it exists for is the automatic // export being off, paused or broken. Nothing here contacts the tax service. type Watchdog struct { payments *payments.Service alert Alerter loc *time.Location log *zap.Logger now func() time.Time // mu guards sent, an in-process record of what has already been raised. The watchdog ticks // daily, so this only prevents a restart from re-alerting within the same day. mu sync.Mutex sent map[string]time.Time } // NewWatchdog builds the deadline watchdog. It returns nil without a payments service or an alert // channel, since it has nothing to watch or nowhere to say it. func NewWatchdog(svc *payments.Service, alert Alerter, loc *time.Location, log *zap.Logger) *Watchdog { if svc == nil || alert == nil { return nil } if loc == nil { loc = time.UTC } return &Watchdog{ payments: svc, alert: alert, loc: loc, log: log, now: func() time.Time { return time.Now().UTC() }, sent: map[string]time.Time{}, } } // Run checks the deadline now and then once per interval, until the context ends. It checks on // start too: a deployment that lands on the 6th should not wait a day to mention a month of // unfiled income. func (w *Watchdog) Run(ctx context.Context, interval time.Duration) { w.tick(ctx) ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: w.tick(ctx) } } } // tick raises the appropriate alert for whatever of the previous month is still unfiled. func (w *Watchdog) tick(ctx context.Context) { now := w.now().In(w.loc) kind, heading := escalation(now.Day()) if kind == "" { return } monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, w.loc) backlog, err := w.payments.MyNalogBacklogBefore(ctx, monthStart) if err != nil { w.log.Error("fiscal deadline check failed", zap.Error(err)) return } if backlog.Count == 0 { return } w.raise(ctx, kind, "«Мой налог»: "+heading, w.body(heading, backlog, monthStart)) } // escalation maps the day of the month onto an alert kind and its heading, or an empty kind on the // days between the warning and the alarm, when there is nothing new to say. func escalation(day int) (kind, heading string) { switch { case day >= overdueDay: return alertBacklogOverdue, "срок подачи вышел" case day >= alarmDay: return alertBacklogDue, "чеки за прошлый месяц не отправлены" case day >= warnDay && day < alarmDay: if day == warnDay { return alertBacklog, "остались неотправленные чеки за прошлый месяц" } } return "", "" } // body renders the alert, naming what is owed and by when. func (w *Watchdog) body(heading string, backlog payments.MyNalogBacklog, monthStart time.Time) string { deadline := time.Date(monthStart.Year(), monthStart.Month(), overdueDay, 0, 0, 0, 0, w.loc) return fmt.Sprintf(`%s. Не зарегистрировано в «Мой налог»: %d приход(ов) на сумму %s. Самый старый — от %s. Крайний срок по закону — %s. Оплата картой считается расчётом электронным средством платежа, поэтому чек по-хорошему формируется в момент расчёта, а не к этой дате. Что сделать: в админ-консоли, раздел «Мой налог», войти в кабинет и запустить выгрузку (или включить автоматический режим, если он выключен).`, heading, backlog.Count, formatAmount(backlog.AmountMinor, string(payments.CurrencyRUB)), backlog.Oldest.In(w.loc).Format("02.01.2006"), deadline.Format("02.01.2006")) } // raise sends an alert unless one of the same kind already went out within the day. func (w *Watchdog) raise(ctx context.Context, kind, subject, body string) { w.mu.Lock() last, seen := w.sent[kind] now := w.now() if seen && now.Sub(last) < 24*time.Hour { w.mu.Unlock() return } w.sent[kind] = now w.mu.Unlock() if err := w.alert.Alert(ctx, kind, subject, body); err != nil { w.log.Error("fiscal deadline alert could not be delivered", zap.String("kind", kind), zap.Error(err)) } }