@crvouga/mockingbird-service-firstpromoter 0.1.0

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 ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog — @crvouga/mockingbird-service-firstpromoter
2
+
3
+ ## 0.1.0 (2026-09-22)
4
+
5
+ Initial release.
package/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # @crvouga/mockingbird-service-firstpromoter
2
+
3
+ Stateful mock of the **FirstPromoter v2** affiliate API for test suites: promoter create (adopt
4
+ before create by `cust_id`), lookups by id / `cust_id` / `ref_token` / email, list, update,
5
+ archive, the dashboard iframe login, signup tracking by click `tid`, `promoter_id` or ref token,
6
+ and the Basic-auth `lead_becomes_referral` webhook our backend receives. Every request is
7
+ recorded in the journal, so a checkout suite can assert what the post-checkout job sent instead
8
+ of producing a failed job.
9
+
10
+ - Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/firstpromoter/SUPPORT.md)
11
+ - FirstPromoter publishes no machine-readable spec: the contract (`openapi.yaml`) is
12
+ hand-authored from our consumer's requests and the zod schemas it validates responses with.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install -D @crvouga/mockingbird-service-firstpromoter
18
+ ```
19
+
20
+ ESM only. Node >= 22 or Bun >= 1.2. No native dependencies. Serve it with
21
+ `npx mockingbird-firstpromoter serve`, `createServer` from `./server` (Node), or `createRuntime`
22
+ with any Fetch server.
23
+
24
+ ## Usage
25
+
26
+ Point `FIRST_PROMOTER_API_URL` at the mock (our Joi schema requires `https`, so either front it
27
+ with TLS or relax that rule in the stack), set any `FIRST_PROMOTER_API_KEY` and
28
+ `FIRST_PROMOTER_ACCOUNT_ID`, and pass the webhook Basic-auth pair as `--webhook-secret`.
29
+
30
+ ```bash
31
+ npx mockingbird-firstpromoter serve --port 8812 \
32
+ --webhook-url http://127.0.0.1:3000/users/webhooks/first-promoter \
33
+ --webhook-secret "$FIRST_PROMOTER_WEBHOOK_AUTH_USERNAME:$FIRST_PROMOTER_WEBHOOK_AUTH_PASSWORD"
34
+ ```
35
+
36
+ ```ts
37
+ import { createRuntime } from "@crvouga/mockingbird-service-firstpromoter"
38
+
39
+ const fp = createRuntime({
40
+ webhooks: { url: "http://127.0.0.1:3000/users/webhooks/first-promoter", secret: "user:pass" },
41
+ })
42
+ const admin = (path: string, body: unknown) =>
43
+ fp.fetch(
44
+ new Request(`http://fp.test/__admin${path}`, {
45
+ method: "POST",
46
+ headers: { "content-type": "application/json" },
47
+ body: JSON.stringify(body),
48
+ }),
49
+ )
50
+
51
+ // An existing affiliate, and a click on their link: the tid checkout stores as fp_tid.
52
+ await admin("/promoters", { email: "nate@example.com", ref_token: "nate91" })
53
+ const { tid } = (await (await admin("/clicks", { ref_token: "nate91" })).json()) as { tid: string }
54
+ // …the app's invoice handler posts POST /v2/track/signup {email, tid}; the mock converts the
55
+ // referral and posts lead_becomes_referral to the backend with Authorization: Basic…
56
+ ```
57
+
58
+ ### Routes
59
+
60
+ | Route | Behaviour |
61
+ | --- | --- |
62
+ | `POST /v2/company/promoters` | `{email, cust_id?, profile?: {first_name, last_name}, drip_emails?, initial_campaign_id?}` → the promoter (`FirstPromoterV2ResponseSchema`), enrolled in the default campaign with a `ref_token` (first name + digits) and `ref_link` `https://gogeviti.com/referrals?fpr=<token>`. A live promoter with the same email or `cust_id` is 422 `{message, errors: {email: ["has already been taken"]}}`. |
63
+ | `GET /v2/company/promoters?page=&per_page=` | `{data: [promoter], meta: {pending_count}}`; archived promoters are left out. |
64
+ | `GET /v2/company/promoters/{value}?find_by=id\|cust_id\|ref_token\|email` | One promoter (archived ones too, with `archived_at`), or 404 `{message: "Promoter not found"}`. |
65
+ | `PUT /v2/company/promoters/{id}` | `{cust_id?, email?, note?, profile?}` → the promoter; 404 / 422 as above. |
66
+ | `POST /v2/company/promoters/archive` | `{ids: [..]}` → a completed batch `{id, status, total, selected_total, processed_count, failed_count, action_label, progress, processing_errors, …}`. |
67
+ | `POST /v2/promoters/iframe_login?promoter_id=` | `{access_token, expires_in}`, or 404. |
68
+ | `POST /v2/track/signup` | `{email, tid? \| promoter_id? \| ref_id?, uid?, skip_email_notification?}` → the referral. An unknown `tid`, promoter or ref token is 404; none of them is 400; an email already tracked is 422. With `autoConvert` (default on) the referral becomes a customer at once and `lead_becomes_referral` is posted. |
69
+
70
+ Every call needs `Authorization: Bearer <key>` and an `Account-ID` header (else 401).
71
+
72
+ ### Webhooks
73
+
74
+ `lead_becomes_referral` bodies match our `FirstPromoterNewCustomerWebhookSchema`: `{event: {id,
75
+ type, created_at}, data: {id, state, email, customer_since, promotion: {id, promoter_id, ref_id,
76
+ leads_count, customers_count, current_referral_reward}, promoter: {id, cust_id, email,
77
+ earnings_balance: {cash}}}}`, sent with `Authorization: Basic base64(<secret>)` where the secret
78
+ is `user:pass`. Non-2xx answers are retried; `GET /__admin/webhooks`, `…/events`, `…/flush` and
79
+ `PUT /__admin/webhook-endpoints` work as usual.
80
+
81
+ ### Admin (beyond the standard contract)
82
+
83
+ | Route | Effect |
84
+ | --- | --- |
85
+ | `POST /__admin/promoters` | Seed a promoter `{email, cust_id?, first_name?, last_name?, ref_token?, campaign_id?}`. |
86
+ | `GET /__admin/promoters` | The namespace's promoters (stored form). |
87
+ | `POST /__admin/clicks` | `{ref_token}` → `{tid, promoterId}`: a click on the promoter's link. |
88
+ | `GET /__admin/referrals` | Tracked signups. |
89
+ | `POST /__admin/referrals/:id/convert` | `{saleAmount?}`: the lead paid; credits the promoter (percent reward × amount) and posts the webhook. |
90
+ | `GET\|PUT /__admin/settings` | `{website?, defaultCampaignId?, autoConvert?, campaigns?}` (campaigns carry `referralRewards` / `promoterRewards` with `coupon`, `amount`, `unit`, `per_of_sale`). The default campaign's referral coupon is `GEVITI50`. |
91
+
92
+ Fault presets (`POST /__admin/faults {"preset": "<name>", "count"?: n}`; `GET /__admin/faults/presets`):
93
+ `created_but_500` (stored, then 500: the retry must adopt by `cust_id`), `lookup_unavailable`
94
+ (lookups 503: neither create nor adopt), `no_campaign` (no `promoter_campaigns`: no ref link),
95
+ `unauthorized`, `rate_limited`, `server_error`, `connection_drop`, `webhook_duplicate`,
96
+ `webhook_drop`.
97
+
98
+ ### Namespaces
99
+
100
+ `x-mockingbird-namespace`, a `/ns/<name>` prefix in `FIRST_PROMOTER_API_URL`, or by API key:
101
+ `PUT /__admin/credentials {"credentials": {"<FIRST_PROMOTER_API_KEY>": "<namespace>"}}`.
102
+
103
+ ### Deliberately not modelled
104
+
105
+ - Error bodies and exact status codes of the real API are unverified (no sandbox credentials):
106
+ errors use `{message}` and validation `{message, errors: {field: [...]}}`. Our consumer only
107
+ branches on `ok` / 404, which the mock gets right.
108
+ - The v1 API our EMR backend still calls (`/v1/promoters/*`, `/v1/track/signup`,
109
+ `/v1/reports/campaigns`); the catalog scopes this mock to v2.
110
+ - Sales, refunds, commissions and payouts beyond the single `convert` step; fraud checks;
111
+ promo codes; the hosted affiliate portal behind the iframe token.
112
+ - Promoter ids are sequential from 4800001 per namespace.
113
+
114
+ ## API
115
+
116
+ | Export | Kind | Description |
117
+ | --- | --- | --- |
118
+ | `FirstPromoterAPI` | class | The in-process mock: `fetch(request)`, `reset()`, `render(promoter)`, `click(refToken)`, `convert(referralId, saleAmount?)`, `seedPromoter(input)`, `state`. Options: `sqlite`, `now`, `namespace`, `settings`, `onWebhook`. |
119
+ | `createRuntime` | function | The mock with the full service contract (health, admin, namespaces, credentials, presets, webhooks). Options: `webhooks: {url, secret, retryDelaysMs?, fetch?}`, `settings`, `clock`, `seed`, `adminKey`, `onLog`, `sqlite`. |
120
+ | `FIRSTPROMOTER_PRESETS` | object | Every named fault preset. |
121
+ | `FIRSTPROMOTER_NAMESPACE` | string | The service name, `"firstpromoter"`. |
122
+ | `WEBHOOK_PATH` | string | Our receiver's path, `/users/webhooks/first-promoter`. |
123
+ | `DEFAULT_CAMPAIGNS`, `DEFAULT_SETTINGS` | values | The seeded campaigns and settings. |
124
+ | `document`, `operationIds`, `supportedOperationIds` | values | The vendored OpenAPI contract and its operation ids. |
125
+ | `createServer`, `serveTarget`, `DEFAULT_PORT` (`./server`) | Node | Serve over `node:http`; the `serve` CLI target; port 8812. |
126
+
127
+ Part of [mockingbird](https://github.com/crvouga/mockingbird).