fix(payments): stop doubling the pack size in buyer mail
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 23s
CI / ui (pull_request) Successful in 1m16s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m50s

Catalog titles already name the pack ("50 фишек"), so appending the chip count
produced "50 фишек — 50 фишек, 79.00 ₽". The letter now carries the title
exactly as the storefront shows it, then the price.

The tax receipt is unaffected: it states the quantity in the wording the tax
service wants, which is a separate surface with separate requirements.
This commit is contained in:
Ilia Denisov
2026-07-28 16:14:53 +02:00
parent f8efa21158
commit 4d4452596d
4 changed files with 19 additions and 13 deletions
+5
View File
@@ -661,6 +661,11 @@ func TestMyNalogBuyerMails(t *testing.T) {
if !strings.Contains(first[0].Text, "не чек") { if !strings.Contains(first[0].Text, "не чек") {
t.Errorf("the purchase letter does not say it is not the receipt:\n%s", first[0].Text) t.Errorf("the purchase letter does not say it is not the receipt:\n%s", first[0].Text)
} }
// The catalog title already names the pack size, so appending it again read as
// "50 фишек — 50 фишек". The line is the title verbatim, then the price, and nothing else.
if !strings.Contains(first[0].Text, "Покупка совершена: test pack, 149.00 ₽.") {
t.Errorf("the purchase line is not the catalog title plus the price:\n%s", first[0].Text)
}
// 2. The receipt letter, once the income is filed, carrying the printable link. // 2. The receipt letter, once the income is filed, carrying the printable link.
if _, err := exp.RunBatch(ctx, time.Minute); err != nil { if _, err := exp.RunBatch(ctx, time.Minute); err != nil {
+11 -8
View File
@@ -185,15 +185,18 @@ func (m *Mailer) stampReceipt(ctx context.Context, mark func(context.Context, uu
} }
} }
// describe renders what was bought in one line. // describe renders what was bought in one line: the catalog title exactly as the buyer saw it on
func describe(title string, chips int, amountMinor int64, currency string) string { // the storefront, then the price.
//
// The pack size is deliberately NOT appended. Catalog titles already name it ("50 фишек"), so
// adding it produced "50 фишек — 50 фишек". The title is the single source of that wording; the
// tax receipt states the quantity separately in the form the tax service wants (see
// mynalog.IncomeName), which is a different surface with different requirements.
func describe(title string, amountMinor int64, currency string) string {
what := strings.TrimSpace(title) what := strings.TrimSpace(title)
if what == "" { if what == "" {
what = "покупка в игре" what = "покупка в игре"
} }
if chips > 0 {
what = fmt.Sprintf("%s — %d фишек", what, chips)
}
return fmt.Sprintf("%s, %s", what, formatAmount(amountMinor, currency)) return fmt.Sprintf("%s, %s", what, formatAmount(amountMinor, currency))
} }
@@ -224,7 +227,7 @@ func purchaseText(p payments.PurchaseMail) string {
отдельным письмом: обычно в течение часа, но иногда это занимает до нескольких дней. отдельным письмом: обычно в течение часа, но иногда это занимает до нескольких дней.
Спасибо, что играете в «Эрудит». Спасибо, что играете в «Эрудит».
`, describe(p.Title, p.Chips, p.AmountMinor, p.Currency), p.OrderID) `, describe(p.Title, p.AmountMinor, p.Currency), p.OrderID)
} }
// receiptText is the fiscal receipt letter, which is how the receipt is actually handed to the // receiptText is the fiscal receipt letter, which is how the receipt is actually handed to the
@@ -240,7 +243,7 @@ func receiptText(r payments.ReceiptMail, inn, baseURL string, loc *time.Location
Продавец применяет налог на профессиональный доход; чек сформирован в сервисе Продавец применяет налог на профессиональный доход; чек сформирован в сервисе
«Мой налог» Федеральной налоговой службы. «Мой налог» Федеральной налоговой службы.
`, describe(r.Title, r.Chips, r.AmountMinor, r.Currency), `, describe(r.Title, r.AmountMinor, r.Currency),
r.OperationTime.In(loc).Format("02.01.2006 15:04"), r.OperationTime.In(loc).Format("02.01.2006 15:04"),
mynalog.ReceiptURL(baseURL, inn, r.ReceiptUUID)) mynalog.ReceiptURL(baseURL, inn, r.ReceiptUUID))
} }
@@ -255,6 +258,6 @@ func cancelText(r payments.ReceiptMail, loc *time.Location) string {
%s %s
Дата покупки: %s Дата покупки: %s
`, describe(r.Title, r.Chips, r.AmountMinor, r.Currency), `, describe(r.Title, r.AmountMinor, r.Currency),
r.OperationTime.In(loc).Format("02.01.2006 15:04")) r.OperationTime.In(loc).Format("02.01.2006 15:04"))
} }
-2
View File
@@ -125,7 +125,6 @@ type PurchaseMail struct {
OrderID uuid.UUID OrderID uuid.UUID
Origin string Origin string
Title string Title string
Chips int
AmountMinor int64 AmountMinor int64
Currency string Currency string
CreatedAt time.Time CreatedAt time.Time
@@ -137,7 +136,6 @@ type ReceiptMail struct {
AccountID uuid.UUID AccountID uuid.UUID
ReceiptUUID string ReceiptUUID string
Title string Title string
Chips int
AmountMinor int64 AmountMinor int64
Currency string Currency string
OperationTime time.Time OperationTime time.Time
+3 -3
View File
@@ -476,7 +476,7 @@ func (s *Store) pendingPurchaseMails(ctx context.Context, limit int) ([]Purchase
rows, err := s.db.QueryContext(ctx, ` rows, err := s.db.QueryContext(ctx, `
SELECT e.event_id, e.account_id, e.order_id, COALESCE(o.origin, ''), SELECT e.event_id, e.account_id, e.order_id, COALESCE(o.origin, ''),
COALESCE(o.expected_amount, 0), COALESCE(o.currency, ''), COALESCE(o.expected_amount, 0), COALESCE(o.currency, ''),
COALESCE(p.title, ''), COALESCE((e.payload->>'chips')::int, 0), e.created_at COALESCE(p.title, ''), e.created_at
FROM payments.payment_events e FROM payments.payment_events e
LEFT JOIN payments.orders o ON o.order_id = e.order_id LEFT JOIN payments.orders o ON o.order_id = e.order_id
LEFT JOIN payments.product p ON p.product_id = o.product_id LEFT JOIN payments.product p ON p.product_id = o.product_id
@@ -495,7 +495,7 @@ func (s *Store) pendingPurchaseMails(ctx context.Context, limit int) ([]Purchase
orderID uuid.NullUUID orderID uuid.NullUUID
) )
if err := rows.Scan(&m.EventID, &m.AccountID, &orderID, &m.Origin, &m.AmountMinor, if err := rows.Scan(&m.EventID, &m.AccountID, &orderID, &m.Origin, &m.AmountMinor,
&m.Currency, &m.Title, &m.Chips, &m.CreatedAt); err != nil { &m.Currency, &m.Title, &m.CreatedAt); err != nil {
return nil, fmt.Errorf("payments: scan purchase mail: %w", err) return nil, fmt.Errorf("payments: scan purchase mail: %w", err)
} }
m.OrderID = orderID.UUID m.OrderID = orderID.UUID
@@ -551,7 +551,7 @@ func (s *Store) pendingMyNalogMails(ctx context.Context, limit int, cancelled bo
&m.AmountMinor, &m.Currency, &m.OperationTime); err != nil { &m.AmountMinor, &m.Currency, &m.OperationTime); err != nil {
return nil, fmt.Errorf("payments: scan receipt mail: %w", err) return nil, fmt.Errorf("payments: scan receipt mail: %w", err)
} }
m.Title, m.Chips = describeSnapshot(snapshot.String) m.Title, _ = describeSnapshot(snapshot.String)
out = append(out, m) out = append(out, m)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {