Files
scrabble-game/ui/src/lib/wallet.test.ts
T
Ilia Denisov 4aa6174968
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m8s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
feat(payments): wallet screen with balances, benefits and storefront
Add the "Кошелёк" section to the settings hub: context-visible chip
balances, active benefits (no-ads term/forever, hints) and a storefront of
chip-priced values and money-priced chip packs. Guests have no wallet; the
Google Play build hides the money purchases behind a RuStore stub; a web
purchase that would draw VK/Telegram chips warns first.

Add the catalog read path the storefront needs — a context-projected
GET /api/v1/user/wallet/catalog (payments service + store, gateway op
wallet.catalog, FBS Catalog/CatalogProduct/CatalogAtom, client decode) —
plus the client leg for the existing wallet.get/buy ops. Value spends reuse
the existing spend path; the chip-pack purchase (money order flow) arrives
with payment intake, so its action is a disabled placeholder for now.

Covered by Go unit (catalog projection) + integration (/wallet/catalog over
Postgres), vitest (formatting, spendable selection, web-spend warning, GP
flag, codec + gateway encode round-trips) and Playwright mock e2e (render,
guest-hidden, GP stub, warning) on Chromium + WebKit.
2026-07-08 12:37:32 +02:00

64 lines
2.7 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import type { Wallet, WalletSegment } from './model';
import { majorAmount, formatAmount, spendableChips, needsWebSpendWarning } from './wallet';
function seg(source: string, chips: number, spendable = true): WalletSegment {
return { source, chips, spendable };
}
function wallet(segments: WalletSegment[]): Wallet {
return { segments, adsForever: false, adsPaidUntilMs: 0, hints: 0 };
}
describe('money formatting', () => {
it('scales roubles by 100 and leaves whole-unit currencies as-is', () => {
expect(majorAmount(14900, 'RUB')).toBe(149);
expect(majorAmount(20, 'VOTE')).toBe(20);
expect(majorAmount(25, 'XTR')).toBe(25);
});
it('formats roubles with two decimals and whole-unit currencies as integers', () => {
expect(formatAmount(14900, 'RUB')).toBe('149.00');
expect(formatAmount(15050, 'RUB')).toBe('150.50');
expect(formatAmount(20, 'VOTE')).toBe('20');
expect(formatAmount(25, 'XTR')).toBe('25');
});
});
describe('spendableChips', () => {
it('sums only the spendable segments', () => {
expect(spendableChips(wallet([seg('direct', 100), seg('vk', 50)]))).toBe(150);
expect(spendableChips(wallet([seg('direct', 100, false), seg('vk', 50, false)]))).toBe(0);
expect(spendableChips(wallet([seg('direct', 100), seg('vk', 50, false)]))).toBe(100);
});
});
describe('needsWebSpendWarning', () => {
it('does not warn when the direct segment alone covers the price', () => {
expect(needsWebSpendWarning('direct', [seg('direct', 200), seg('vk', 400)], 150)).toBe(false);
});
it('warns when the priority draw reaches into a store (vk/tg) segment', () => {
// direct 120 + vk covers the rest of a 300 price → touches vk
expect(needsWebSpendWarning('direct', [seg('direct', 120), seg('vk', 400)], 300)).toBe(true);
});
it('warns when only a store segment is spendable and it covers the price', () => {
expect(needsWebSpendWarning('direct', [seg('vk', 400)], 100)).toBe(true);
expect(needsWebSpendWarning('direct', [seg('telegram', 400)], 100)).toBe(true);
});
it('does not warn when the price exceeds all spendable chips (buy is disabled anyway)', () => {
expect(needsWebSpendWarning('direct', [seg('direct', 120), seg('vk', 100)], 500)).toBe(false);
});
it('never warns inside a VK or Telegram context (spend stays in the same store segment)', () => {
expect(needsWebSpendWarning('vk', [seg('vk', 400)], 100)).toBe(false);
expect(needsWebSpendWarning('telegram', [seg('telegram', 400)], 100)).toBe(false);
});
it('ignores non-spendable segments (a frozen or untrusted wallet never warns)', () => {
expect(needsWebSpendWarning('direct', [seg('vk', 400, false)], 100)).toBe(false);
});
});