# Partner and referral hierarchy foundation

## Scope

Phase 9A1 introduces current Partner status and referral-resolution data only. It does not calculate, discount, credit, debit, or move money.

## Partner state and allocation

`users.is_partner` is an explicit, admin-managed capability. Admin status never implies Partner status. A Partner has one `partner_profiles` record with integer basis-point allocations. The default is 5,000 commission bps and 5,000 discount bps (50/50). Both fields must be in `0..10000` and must total exactly `10000`.

Removing Partner status keeps the profile for a possible future reactivation; it never changes the referral graph.

## Referral hierarchy

`users.referred_by_user_id` remains the one direct-introducer relationship. `directReferrer()` returns that immediate user. `ancestorChain()` walks upward with visited IDs and a 100-level cap to safely tolerate corrupted cycles. Self-referral is rejected by the User model on normal persistence.

The **top Partner** is the highest Partner in the ancestry, not the closest. For `Ali (Partner) -> Bakar (Partner) -> Abu -> Amin`, Amin's direct referrer is Abu, the direct Partner for Abu is Bakar, and Amin's top Partner is Ali. A chain with no Partner has no top Partner (`null`).

## Global settings

The authoritative system-setting keys are `partner.first_payment_benefit_bps`, `partner.renewal_commission_bps`, and `non_partner.first_membership_commission_bps`. They store integers in basis points. The admin dashboard accepts percentage inputs and converts them server-side; clients cannot submit arbitrary setting keys. Monthly membership price remains independently authoritative in `membership.monthly_price_minor`.

## Membership referral pricing resolver

`MembershipReferralPricingService` is the sole Phase 9A2 calculator. It reads the current server-side membership price, global settings, direct referrer, current Partner status, Partner profile, and referral hierarchy, then returns an immutable `MembershipReferralPricingResult`. It never creates an order, payment attempt, commission, wallet transaction, or any other database record. Current Partner status is authoritative at calculation time because there is no historical Partner-status snapshot yet.

For a first paid membership with a direct Partner referrer, the benefit pool is `floor(base_minor * partner_first_payment_benefit_bps / 10000)`. Partner commission is `floor(benefit_pool * commission_allocation_bps / 10000)` and buyer discount is `floor(benefit_pool * discount_allocation_bps / 10000)`. The buyer pays `base - discount`; TakafulHub net is `payable - commission`. A missing Partner profile or required rate is a controlled pricing failure, never a silent zero-value fallback.

For a first paid membership with a direct non-Partner referrer, the buyer receives no discount and the direct referrer receives `floor(base_minor * non_partner_first_membership_commission_bps / 10000)`. A higher Partner in the ancestry receives nothing for that first payment. With no direct referrer, there is no discount or commission.

For a renewal without a Coupon, the buyer pays the full current membership price. With an eligible Coupon, the buyer pays the post-Coupon amount. The highest current Partner in the ancestry (the top Partner) receives `floor(amount_payable_minor * partner_renewal_commission_bps / 10000)`, not a percentage of the original base. For example, RM100 minus a RM20 Coupon is RM80 payable; at 20%, the Partner receives RM16 and TakafulHub net is RM64. A chain without a Partner produces no renewal commission; a direct non-Partner receives no renewal commission.

All arithmetic uses integer minor units and `intdiv`, which deterministically truncates toward zero (equivalent to floor for these non-negative amounts). The result preserves accounting: base equals TakafulHub net plus discount plus the applicable commission.

## MembershipOrder snapshots

When a MembershipOrder is created through `MembershipOrderSnapshotService`, the current resolver result is copied into system-controlled snapshot fields: base, discount, resolved payable amount, TakafulHub net, direct referrer, top Partner, commission recipient/type, applicable rates and allocations, and exact commission amounts. `pricing_context` permanently records `first_paid_membership` or `renewal`; it is not inferred later from current membership state. A prior admin membership activation does not count as a paid order. A prior fulfilled MembershipOrder does make the next order a renewal.

`amount_minor` is now the immutable buyer charge for every newly created MembershipOrder and is written equal to `amount_payable_minor`. CHIP creates and verifies its provider purchase against this stored amount; E-Wallet debits this same stored amount. `base_amount_minor` remains the official pre-discount membership price. Legacy historical orders with no payable snapshot continue to use their existing `amount_minor`; no financial values are inferred or rewritten. No existing order is recalculated when settings, allocations, Partner status, or referral links later change.

Snapshot fields are not fillable through ordinary model/browser paths. The snapshot service accepts only operational order attributes such as payment method/provider and ignores any caller-provided pricing, rate, recipient, hierarchy, or commission fields.

The buyer-facing membership page may show only the base price, a neutral **Special Referral Discount**, and amount payable. It never reveals Partner status, commission, allocation, rates, hierarchy, or a commission recipient.

During this temporary integration boundary, the existing legacy AffiliateCommission flow remains active for a direct non-Partner's first successful paid order. A direct Partner-referral snapshot is explicitly excluded from that legacy flow, preventing a legacy 25% E-Wallet credit on top of the stored Partner liability. Partner and renewal payouts remain deferred.

## Partner commission wallet and payout

The Partner Wallet is a separate withdrawable-later financial domain; it never reuses the normal E-Wallet, which remains closed-loop for membership and lead-credit purchases. `partner_wallets` stores the balance and `partner_wallet_transactions` is an append-only ledger recording integer amount, balance before/after, MembershipOrder reference, and metadata.

After a verified payment fulfils a MembershipOrder, `PartnerCommissionPayoutService` reads only that order's immutable snapshot. For `partner_first_payment`, it credits exactly `partner_first_commission_minor` to the snapshotted recipient through `PartnerWalletService`, then writes one `partner_commission_payouts` audit record. The payout table has a unique MembershipOrder reference; order and wallet row locks protect exactly-once crediting under duplicate fulfilment or callback delivery. A zero-value Partner commission creates neither wallet nor payout record.

The snapshot is the historical entitlement. If a Partner is removed after checkout but before fulfilment, the valid snapshot recipient is still credited; wallet creation is permitted specifically for that historical entitlement. Settings, allocation, price, and hierarchy are never re-resolved. Direct non-Partner first-payment commission remains in legacy `AffiliateCommission` and normal E-Wallet. Direct Partner snapshots are excluded from that legacy path.

For a fulfilled `renewal` order with `partner_renewal`, the same payout service credits exactly `partner_renewal_commission_minor` to the snapshotted **top Partner**. A direct non-Partner receives no renewal affiliate commission, and a renewal snapshot with no Partner produces no payout. CHIP callbacks and internal E-Wallet fulfilment use this identical snapshot-only payout path. The unique payout audit record and locked order/wallet rows retain exactly-once behaviour under duplicate fulfilment or callback delivery.

Withdrawals, holds, releases, admin approval, receipts, PDFs, cash-out, and Partner Wallet UI remain deferred.

## Confidential Partner portal

The private `/partner` workspace is guarded by current `is_partner` status and is unavailable immediately after Partner status removal, even though historical wallet and payout records remain retained. Navigation receives only the current user's minimal `is_partner` capability; wallet balances, payout history, rates, allocations, and hierarchy data are loaded only on the protected Partner route.

Partners can see their own Partner Wallet balance (separate from the normal E-Wallet), owned commission payout history, aggregate first/renewal totals, direct-referral count, existing affiliate registration link, current allocation, permitted Partner program rates, and a server-calculated first-membership preview. History is paginated and scoped to the current Partner. It shows only safe date/type/member-name/amount/status fields, never payment references, raw metadata, IC data, other wallets, or network hierarchy.

Partners may change only their own commission/discount allocation, which must always total 100%. They cannot change membership pricing, global benefit/renewal rates, non-Partner rates, or another user's allocation.

## Partner withdrawal requests

Current Partners may request a withdrawal only from their own Partner Wallet. `balance_minor` is the total earned balance; `reserved_minor` is the sum held for pending (and, in a future phase, approved) withdrawal requests; `available_minor` is `balance_minor - reserved_minor`. All values are integer minor units. A request locks the wallet row, verifies a positive amount does not exceed available balance, increments `reserved_minor`, and writes one pending `partner_withdrawals` audit row in the same transaction. It does not debit the total wallet balance and does not create a misleading balance-ledger transaction.

Pending requests are shown only in the protected Partner portal, with requested date, amount, and status. Browser-supplied wallet, user, reservation, status, or admin fields are not accepted. Normal E-Wallet and Lead Credit balances are never used. New Partner commission credits increase total and available balance while leaving existing reservations unchanged. Historical wallets and requests remain stored after Partner removal, but the removed user immediately loses portal and request access.

Future rejection will release the reserved amount; future payment will release the reservation and debit total balance atomically. This phase has no admin list, approval, rejection, payment, bank details, payment reference workflow, receipt, PDF, or payout integration.

## Admin withdrawal review

Only administrators can list, filter, inspect, approve, reject, or mark paid withdrawal requests. Admin transitions consistently lock the withdrawal and then its Partner Wallet. A pending request may become approved, which records the admin and timestamp but keeps the reservation held. Alternatively it may become rejected, which records the admin and timestamp and releases exactly that request's reserved amount without changing total earned balance. An approved request may be marked paid once with a required payment reference: its reservation is released, its total wallet balance is debited, and one immutable `withdrawal_paid` ledger entry records the before/after total and withdrawal reference. A non-permitted transition or integrity failure refuses the action without mutation.

## Official withdrawal receipt PDF

Every receipt PDF is rendered solely from the immutable `partner_withdrawal_receipts` snapshot: it never reads current company settings, Partner profile data, payment data, withdrawal values, or receipt numbering. Paid-only receipt downloads are protected: a current Partner may download only their own receipt; an admin may download any paid withdrawal receipt; a removed historical Partner has no Partner-route access while admin access remains. Missing historical logo files are a safe logo-free fallback. Downloading is a pure read: it does not regenerate a receipt, change the receipt snapshot, increment the annual sequence, or mutate the withdrawal, wallet, or ledger. PDF storage, email, editing/regeneration, bank integration, and invoices remain deferred.

## Financial invariants

For direct-Partner first payments, `base = discount + Partner commission + TakafulHub net`; all values are integer minor units and the MembershipOrder snapshot is authoritative after creation. A direct non-Partner receives only the normal first-payment E-Wallet commission; higher Partners receive no first-payment payout. Coupon renewals preserve `base = coupon discount + Partner renewal commission + TakafulHub net`; the renewal commission is snapshotted from the post-Coupon payable amount.

Partner Wallet reservation always satisfies `0 <= reserved <= balance` and `available = balance - reserved`. Approval leaves all three values unchanged. Rejection releases exactly the request amount from reserved without debiting balance. Paid releases that reservation and debits the same amount from total balance, preserving the available amount. Locked snapshot/order/wallet transitions and unique payout, receipt, and receipt-number records form the exactly-once boundaries for fulfilment, withdrawal payment, and receipts.

## Confidentiality and authorization

`is_partner` is the only Partner-related shared capability and is limited to the current authenticated user for navigation. Partner rates, profiles, wallets, payouts, withdrawals, receipt settings, allocation, hierarchy, and top-Partner information are loaded only through protected, curated read models. Current Partner status is mandatory for every Partner portal, allocation, withdrawal, and receipt route; removal immediately revokes those routes while preserving historical records for admin audit.

Partner resources are owner-scoped, including receipt downloads. Admin assignment, fixed-key financial settings, withdrawal lifecycle actions, receipt settings, and admin receipt access require the admin middleware. Browser inputs never authoritatively control wallet IDs, users, financial snapshot values, payout recipients, status transitions, receipt numbers, or receipt snapshots: checkout snapshots and withdrawal transitions derive those values from persisted/server-side state. No ordinary route updates referral ancestry or receipt snapshots. Financial models are returned through curated arrays rather than global model serialization, and Partner flows do not log financial payloads or IC data.

## Confidentiality and future work

Normal shared authenticated props do not include Partner rates, another user's Partner status/allocation, or hierarchy details. No Partner navigation or portal is exposed. A future phase may persist resolver results into an order snapshot and integrate fulfilment. This phase deliberately adds no Partner commission transaction, checkout discount, Partner wallet, withdrawal, receipt, cash-out, or membership-order behaviour.
