@whooshbang/contract-mock 2.0.0-rc.5 → 2.0.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ recorded here.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 2.0.0-rc.6
9
+
10
+ ### Added
11
+
12
+ - Deterministic prepaid billing, recurring entitlement and quoted subscription-change scenarios, including insufficient funding and paid attribution enforcement through the rc.6 contract (FXO-2275, FXO-2276, FXO-2814).
13
+
8
14
  ## 2.0.0-rc.5
9
15
 
10
16
  ### Changed
package/README.md CHANGED
@@ -31,9 +31,8 @@ const response = await mock.fetch(
31
31
  );
32
32
  ```
33
33
 
34
- The organization contract batch pairs mock `1.0.0-rc.35` with contracts
35
- `1.0.0-rc.23` and SDK `1.0.0-rc.32`. Install that batch together after
36
- publication; the tenant rename has no old-route or old-field aliases.
34
+ Install contracts, SDK and mock from the same published candidate batch.
35
+ The tenant rename has no old-route or old-field aliases.
37
36
 
38
37
  The mock also executes the organization-scoped administration contract. Use
39
38
  `humanSession` (or `otherHumanSession` for isolation tests) with
@@ -41,6 +40,73 @@ The mock also executes the organization-scoped administration contract. Use
41
40
  `GET /v1/projects/{project_id}`. Project creation atomically returns synthetic
42
41
  `test` and `live` environments and one `Default` notifier for each.
43
42
 
43
+ ## Billing consumer tests
44
+
45
+ The mock executes the public wallet, recurring Checkout, billing portal,
46
+ automatic top-up settings/card setup, project slot and attribution routes. Organization administrators may use a
47
+ human session; OAuth grants also need explicit `billing:read` or
48
+ `billing:write`. Project, organization API and machine credentials cannot
49
+ perform financial operations. Attribution uses the organization-level
50
+ `projects:read` and `projects:write` scopes, with administrator membership for
51
+ changes. The MCP tools retain their own scope and action-confirmation checks.
52
+
53
+ Each account starts with the once-only $2 starter credit and one project slot.
54
+ The fixture project already occupies that slot. Multi-project consumer tests
55
+ must purchase extra slots through recurring Checkout or release the old slot
56
+ with `assignBillingProjectSlots` before creating its replacement. Old projects
57
+ retain their data and recorded test access; new live sends require an active
58
+ slot. The default recurring catalog contains the $5 attribution and project
59
+ add-ons. Team/Agency prices and allowances are not invented: pass synthetic
60
+ offers through `billing.offers` when a consumer test needs a base plan.
61
+
62
+ Wallet purchases use the approved $5/$10/$25/$50/$100 packs and
63
+ $5/$10/$30/$65/$150 total core credit. Purchased and bonus funds remain
64
+ separate. One accepted live notification debits $0.003 for core delivery;
65
+ shared email adds $0.001 from purchased or starter credit. Paid allowances are
66
+ used first. Tests, provider retries, later failures and answers do not create
67
+ extra charges. Payment replay cannot fund the same Checkout twice.
68
+
69
+ These are synthetic payments. Creating Checkout returns a local fixture URL;
70
+ reconciliation applies the fixture's payment outcome, not a query parameter
71
+ or caller-supplied `paid` flag. No Stripe request, card charge or webhook proof
72
+ occurs. Set `billing.checkoutOutcome` to a callback returning `paid`, `unpaid`
73
+ or `expired` to exercise recovery. `billing.accounts` may seed exact decimal
74
+ balances for boundary tests. State resets with `control.reset()`.
75
+
76
+ Automatic top-up starts disabled and requires a confirmed pack, threshold and
77
+ gross monthly cap covering that pack. The mock's card setup is synthetic:
78
+ `billing.autoTopUpSetupOutcome` returns `ready`, `unpaid` or `expired`. It models
79
+ owned setup recovery and rejects stale completion after settings change; it
80
+ does not run a bank charge or a scheduled automatic-purchase engine. Saving a
81
+ card does not grant delivery credit. Actual automatic payment capture, pending
82
+ reservations and gross monthly cap enforcement are tested against the owned
83
+ storage and Stripe SDK boundaries.
84
+
85
+ ```ts
86
+ const mock = createWhooshBangContractMock({
87
+ billing: {
88
+ checkoutOutcome: () => "unpaid",
89
+ accounts: [{
90
+ organizationId: "organization_synthetic",
91
+ starterCreditUsd: "0.003000",
92
+ }],
93
+ },
94
+ });
95
+ ```
96
+
97
+ Paid add-on reconciliation enables the server-enforced attribution preference
98
+ until its paid boundary. Restoring the preference restores visible attribution;
99
+ expiry preserves the preference but removes its paid effect. Paid wallet
100
+ purchases or current recurring periods select 90-day content retention;
101
+ starter-only accounts select seven days. The mock projects that entitlement;
102
+ it does not simulate the storage purge jobs or prove real recipient rendering.
103
+
104
+ Each project has one webhook endpoint across its environments. Paused and
105
+ unverified endpoints still occupy it; disabling one releases it. Only a
106
+ terminal failed automatic cycle can receive the one included manual replay.
107
+ `control.runPending()` executes that cycle using the same eight-attempt bound.
108
+ The original cycle and manual recovery together cannot exceed 16 attempts.
109
+
44
110
  The same human session can execute the environment credential lifecycle:
45
111
  `GET/POST /v1/projects/{project_id}/environments/{environment_id}/credentials`
46
112
  and the nested `rotate` and `revoke` actions. The mock emits a deterministic
@@ -0,0 +1,77 @@
1
+ import type { AutoTopUpSettings, UpdateAutoTopUpSettingsRequest, AutoTopUpSetup, BillingAccount, BillingEntitlements, BillingProjectSlots, ContractValidator, CreateSubscriptionCheckoutRequest, Problem, ProblemCode, Project, ProjectAttribution, SubscriptionCheckout, SubscriptionChange, WalletCheckout } from "@whooshbang/contracts";
2
+ import type { Hono } from "hono";
3
+ import type { ContractMockBillingOptions, OrganizationScope } from "./types.js";
4
+ interface CheckoutState {
5
+ readonly organizationId: string;
6
+ readonly resource: WalletCheckout | SubscriptionCheckout;
7
+ readonly purchaseCents?: number;
8
+ readonly selection?: CreateSubscriptionCheckoutRequest;
9
+ readonly selectedOffers?: BillingEntitlements["offers"];
10
+ applied: boolean;
11
+ }
12
+ /** Local, isolated financial fixtures. All state resets with the mock. The
13
+ * production service independently verifies Stripe objects and signatures. */
14
+ export declare class MockBillingState {
15
+ private readonly options;
16
+ readonly cardSetups: Map<string, {
17
+ organizationId: string;
18
+ revision: number;
19
+ resource: AutoTopUpSetup;
20
+ applied: boolean;
21
+ }>;
22
+ private readonly automaticSettings;
23
+ private readonly accounts;
24
+ readonly checkouts: Map<string, CheckoutState>;
25
+ readonly changes: Map<string, {
26
+ organizationId: string;
27
+ resource: SubscriptionChange;
28
+ selectedOffers: BillingEntitlements["offers"];
29
+ }>;
30
+ readonly attribution: Map<string, boolean>;
31
+ readonly replays: Map<string, {
32
+ fingerprint: string;
33
+ status: number;
34
+ body: unknown;
35
+ diagnosticId: string;
36
+ }>;
37
+ readonly offers: BillingEntitlements["offers"];
38
+ private sequence;
39
+ readAutoTopUp(organizationId: string): AutoTopUpSettings;
40
+ autoTopUpRevision(organizationId: string): number;
41
+ updateAutoTopUp(organizationId: string, input: UpdateAutoTopUpSettingsRequest): AutoTopUpSettings;
42
+ createCardSetup(organizationId: string): AutoTopUpSetup | null;
43
+ reconcileCardSetup(organizationId: string, id: string): "applied" | "unpaid" | "ignored";
44
+ constructor(options?: ContractMockBillingOptions);
45
+ private account;
46
+ readAccount(organizationId: string): BillingAccount;
47
+ private changeOffers;
48
+ private selectionTotal;
49
+ createChange(organizationId: string, selection: CreateSubscriptionCheckoutRequest, now: string): SubscriptionChange | null;
50
+ settleChange(organizationId: string, id: string, now: string): SubscriptionChange | null;
51
+ confirmChange(organizationId: string, id: string, charge: string, now: string): SubscriptionChange | null;
52
+ readEntitlements(organizationId: string, now: string): BillingEntitlements;
53
+ readAttribution(organizationId: string, projectId: string, now: string): ProjectAttribution;
54
+ readSlots(organizationId: string, projects: readonly Project[], now: string): BillingProjectSlots;
55
+ assignSlots(organizationId: string, ids: readonly string[]): void;
56
+ addProject(organizationId: string, id: string): void;
57
+ /** Call once at acceptance, after validation/replay/routing. Provider work and
58
+ * answers never call this function, so retries and later failures do not debit. */
59
+ hasCoreFunding(organizationId: string, now: string): boolean;
60
+ fundLive(organizationId: string, sharedEmail: boolean, now: string): "wallet_insufficient" | "email_credit_insufficient" | null;
61
+ createCheckout(organizationId: string, selection: number | CreateSubscriptionCheckoutRequest): CheckoutState | null;
62
+ reconcile(checkout: CheckoutState, now: string): "applied" | "unpaid" | "ignored";
63
+ }
64
+ interface BillingHooks {
65
+ state(): MockBillingState;
66
+ now(): string;
67
+ projects(): readonly Project[];
68
+ fingerprint(value: unknown): string;
69
+ authorize(request: Request, scope: OrganizationScope, admin: boolean): string | Response;
70
+ fail(code: ProblemCode, status: Problem["status"], fieldCode?: string, detail?: string): Response;
71
+ reply<T>(body: T, validator: ContractValidator<T>, status?: number): Response;
72
+ read<T>(request: Request, validator: ContractValidator<T>): Promise<T | Response>;
73
+ }
74
+ /** Register only published public resources. Synthetic vendor completion is
75
+ * driven by a programmatic fixture, never a caller-supplied payment flag. */
76
+ export declare function addMockBillingRoutes(app: Hono, hooks: BillingHooks): void;
77
+ export {};