# Domain: Accounts, Content, Shops, Identity ## Customer accounts (`modules/account/`) Three buckets per customer: `available` and `frozen` (monetary, per currency) plus `points` (no currency). Balances change only through `credit`/`debit`/`freeze`/`release`, each appending one immutable `customer_account_entries` row in the same transaction as the business write. The ledger is the audit trail; balances are never set absolutely. Credits in a currency the customer never held lazily create the zero row (`ensure_monetary_account`). ## Wallet (`modules/wallet/`) Buyer- and shop-owner-facing entry points over the ledger. `GET /wallet` is the available/frozen summary in the platform base currency; `GET /wallet/entries` pages the caller's own `customer_account_entries` (money kinds only) newest first with signed deltas and resulting balances. `POST /wallet/recharges` is a **simulated** demo credit (`demo: true` on the payload, no payment provider), and `POST /wallet/withdrawals` freezes the requested amount out of available balance through the account module's guarded transfer. Platform admins list and review pending applications via `/admin/wallet/withdrawals`: approve consumes the frozen funds, reject returns them to available, and the `pending -> approved|rejected` transition is guarded so a repeat review is a 409. Every movement pairs with a ledger entry in the same transaction. ## Settlement (`modules/settlement/`) Per-shop, per-period reconciliation statements generated manually for a closed week or month (`/shop/settlement/*` for the own shop, `/admin/settlement/*` for the platform). Generation is idempotent per `(shop, period_kind, period_start)` — enforced by a unique index — and snapshots the contributing confirmed-received orders (`orders.completed_at` inside the period), their gross and completed refunds converted into the platform base currency, the platform commission rate in integer basis points, and `payable = gross - refunds - commission`, all in integer minor units. The platform rate lives in `platform_settings` (`settlement.commission_rate_bps`) and only affects statements generated after a change. Confirmation is a guarded `pending -> confirmed` transition that credits the payable amount to the shop owner's available account with exactly one `settlement_payout` ledger entry. ## Storefront content (`modules/content/`) Four ordered home-content kinds (banners, promos, quick links, floor adverts), platform-managed by whole-list replacement (`PUT /admin/content/{kind}` reindexes positions atomically). Quick-link glyphs are inline SVG path data. ## Shops (`modules/shop/`) Platform creates/suspends shops; shop profiles are a side table with bilingual address/notice/after-sale and platform-owned scores. Merchants edit their own profile via `PUT /api/shop/profile` — scores are platform-only and the merchant upsert never touches them. ## Identity (`modules/identity/`, `src/auth.rs`) Email+password register/login, JWT bearer tokens. `AuthUser` carries `id/role/shop_id`; `require(&[roles])` for RBAC, `own_shop()` / `require_shop()` for tenant scoping (cross-shop resources return 404). `ensure_accounts` runs inside the registration transaction. ## Merchant onboarding (`modules/merchant_onboarding/`) The B2B entry that replaces manual admin shop creation: an authenticated user submits one application as `personal` or `enterprise` (kind-specific entity fields, one or more reference categories, contact details, qualification URLs only). One live application per user is enforced by service validation plus a partial unique index on `(user_id) WHERE status IN ('pending','approved')`, so a rejected applicant may re-apply. The review state machine is `pending -> approved | rejected` through guarded updates; rejection requires a non-empty reason, and both terminal states are immutable (409 on repeat). Approval is one transaction — `shop::service::create_in_tx`, a dedicated `shop_owner` user via `identity::repo::insert_user`, its zero-balance accounts, and the guarded status flip — and returns the generated initial password exactly once; any failure rolls the whole provisioning back and leaves the application `pending`. Applicants only ever read their own history; platform admins list/filter all applications. ## Membership (`modules/membership/`) Platform-managed `member_levels` (bilingual name and benefits, icon, globally unique integer growth threshold) plus an append-only `growth_logs` ledger and `users.level`. When the buyer confirms receipt and the order reaches `completed`, `membership::service::accrue_for_order` runs inside that transaction: it converts the order's realized paid amount (total minus completed refunds) into the base currency with `money::convert_minor`, truncates to whole units via the base exponent, appends exactly one entry (`ON CONFLICT DO NOTHING` on the `(user_id, reference_type, reference_id)` partial unique index), and moves `users.level` with a guarded update that only ever raises the threshold. Leveling is one-way; the status read re-derives the displayed level against the current thresholds so an admin edit is reflected without rewriting members. Growth history is own-only and paginated. ## Messaging (`modules/messaging/`) Per-customer system messages emitted by order events: payment success (`order_paid`), shipment dispatch (`order_shipped`), and refund completion (`refund_completed`, referenced by the after-sale row and naming the order). Each emit is a guarded insert keyed by `(user_id, kind, reference_type, reference_id)` inside the transition's transaction, so a retried handler cannot duplicate a message and a soft-deleted row still occupies its slot. Messages are created `unread`; marking one or all read uses guarded updates that only touch `unread` rows, deletion is a soft-delete marker that is idempotent and excludes the row from listing and counting, and a dedicated unread-count endpoint feeds the mall's top-bar badge. ## Key files See `docs/code_index/platform.md`.