package mynalogsync import ( "context" "time" "go.uber.org/zap" "scrabble/backend/internal/account" ) // Run drives the automatic export until the context ends. // // A tick with nothing to do costs one local query: the queue is read before anything authenticates, // so an installation with no unfiled income never appears at the tax service at all. The automatic // mode is armed from the admin console rather than from configuration, so that it can be switched // on only once the operator has watched a run go through by hand. func (e *Exporter) Run(ctx context.Context, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: e.tick(ctx) } } } // tick performs one automatic run, if the rail is signed in, armed and not paused. func (e *Exporter) tick(ctx context.Context) { stored, ok, err := e.payments.MyNalogSession(ctx) if err != nil { e.log.Error("mynalog session could not be read", zap.Error(err)) return } if !ok || !stored.AutoEnabled || stored.Paused() { return } sum, err := e.RunBatch(ctx, WorkerBudget) if err != nil { e.log.Error("mynalog automatic export failed", zap.Error(err)) return } if sum.Registered == 0 && sum.Cancelled == 0 && sum.NotRequired == 0 && sum.Failed == 0 && sum.Unknown == 0 { return // nothing happened; not worth a log line every quarter of an hour } e.log.Info("mynalog automatic export ran", zap.Int("registered", sum.Registered), zap.Int("cancelled", sum.Cancelled), zap.Int("not_required", sum.NotRequired), zap.Int("failed", sum.Failed), zap.Int("unknown", sum.Unknown), zap.String("stopped", sum.Stopped)) } // MailAlerter delivers operator alerts by email, reusing the relay the admin digest already uses. type MailAlerter struct { mail account.Mailer from string to string } // NewMailAlerter builds the alert channel. It returns nil unless a distinct admin sender and // recipient are configured, matching how the existing admin digest stays inert without them. func NewMailAlerter(mail account.Mailer, from, to string) *MailAlerter { if mail == nil || from == "" || to == "" { return nil } return &MailAlerter{mail: mail, from: from, to: to} } // Alert sends one operator alert. kind is the caller's deduplication key and is deliberately not // put in the message. // // The body never carries a link into the admin console: an admin URL must not travel by email, // where a provider could cache or index it (the same rule the admin digest follows). func (a *MailAlerter) Alert(ctx context.Context, _, subject, body string) error { return a.mail.Send(ctx, account.Message{ From: a.from, To: a.to, Subject: subject, Text: body, }) }