b6c2598710
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 26s
CI / ui (pull_request) Successful in 1m13s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Failing after 16m19s
Robokassa moderation requires the public offer to list every digital good with its price. Move /offer/ off the static landing container to the render sidecar: it splices the live catalog price list (§4.4) into the owner-edited ui/legal/offer_ru.md and renders it with the shared ui/src/lib/offer.ts — one renderer, no drift, always matching the current catalog with no redeploy. - backend: /api/v1/internal/offer/pricing (internal, off the edge allow-list) projects the active catalog into two markdown tables — chip packs priced per rail (roubles / VK votes / Telegram Stars) and chip-priced values — through payments.Money so no float reaches the page. Cached in memory: warmed at boot, marked stale on every catalog mutation, so a served render issues no query. - renderer: GET /offer/ fetches the tables and substitutes them at the <#pricing_template#> marker, then renders; offer_ru.md is baked into the image and marked is bundled from ui. GET /offer -> 301. Only /offer/ is edge-exposed. - caddy: route /offer/ to the sidecar; drop the now-dead landing /offer/ handlers and the vite emit-offer plugin. - offer: fill §4.3 (the chip-payment wording) and drop the in-page back link. - landing footer: a feedback link (the offer's Telegram contact) beside the offer link. - docs (ARCHITECTURE, FUNCTIONAL +_ru, renderer README), CI /offer/ probe, unit + integration + node tests.
70 lines
2.4 KiB
Go
70 lines
2.4 KiB
Go
package payments
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// TestProjectOfferPricing checks the happy path: a chip pack priced on every rail and a chip-priced
|
|
// value render into the two tables, pack table first, money formatted through Money.
|
|
func TestProjectOfferPricing(t *testing.T) {
|
|
entries := []catalogEntry{
|
|
{
|
|
id: uuid.New(),
|
|
title: "50 «Фишек»",
|
|
atoms: []atomQty{{atomType: atomChips, quantity: 50}},
|
|
prices: []priceRow{
|
|
{method: string(SourceDirect), currency: CurrencyRUB, amount: 20000},
|
|
{method: string(SourceVK), currency: CurrencyVote, amount: 30},
|
|
{method: string(SourceTelegram), currency: CurrencyStar, amount: 100},
|
|
},
|
|
},
|
|
{
|
|
id: uuid.New(),
|
|
title: "200 подсказок",
|
|
atoms: []atomQty{{atomType: "hints", quantity: 200}},
|
|
prices: []priceRow{{method: "", currency: CurrencyChip, amount: 50}},
|
|
},
|
|
}
|
|
md := projectOfferPricing(entries)
|
|
for _, want := range []string{
|
|
"| Наименование | Рубли | Голоса в VK | Stars в Telegram |",
|
|
"| 50 «Фишек» | 200.00 | 30 | 100 |",
|
|
"| Наименование | «Фишки» |",
|
|
"| 200 подсказок | 50 |",
|
|
} {
|
|
if !strings.Contains(md, want) {
|
|
t.Errorf("projection missing %q\n---\n%s", want, md)
|
|
}
|
|
}
|
|
if strings.Index(md, "Приобретение") > strings.Index(md, "Использование") {
|
|
t.Errorf("the pack table must precede the values table:\n%s", md)
|
|
}
|
|
}
|
|
|
|
// TestProjectOfferPricingMissingRailAndEscaping checks a pack missing a rail shows an em dash and a
|
|
// title carrying a pipe is escaped so the table layout survives.
|
|
func TestProjectOfferPricingMissingRailAndEscaping(t *testing.T) {
|
|
entries := []catalogEntry{{
|
|
id: uuid.New(),
|
|
title: "Bonus | pack",
|
|
atoms: []atomQty{{atomType: atomChips, quantity: 10}},
|
|
// A roubles price only — no VK, no Telegram.
|
|
prices: []priceRow{{method: string(SourceDirect), currency: CurrencyRUB, amount: 9900}},
|
|
}}
|
|
md := projectOfferPricing(entries)
|
|
if want := `| Bonus \| pack | 99.00 | — | — |`; !strings.Contains(md, want) {
|
|
t.Errorf("want row %q in:\n%s", want, md)
|
|
}
|
|
}
|
|
|
|
// TestProjectOfferPricingEmpty checks an empty catalog projects to the empty string (no stray table
|
|
// headers), so the offer's pricing marker is replaced with nothing.
|
|
func TestProjectOfferPricingEmpty(t *testing.T) {
|
|
if got := projectOfferPricing(nil); got != "" {
|
|
t.Errorf("empty catalog must project to empty string, got %q", got)
|
|
}
|
|
}
|