@waffo/pancake-ts 0.1.9 → 0.2.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 CHANGED
@@ -4,22 +4,23 @@ All notable changes to `@waffo/pancake-ts` will be documented in this file.
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [0.1.9] - 2026-04-02
7
+ ## [0.2.0] - 2026-04-02
8
8
 
9
9
  ### Added
10
10
 
11
11
  - **Checkout convenience methods** — `client.checkout.authenticated.create()` and `client.checkout.anonymous.create()` wrap the full checkout flow into a single call. Authenticated mode issues a session token, creates a checkout session, and returns a URL with the token appended as a URL fragment. Anonymous mode creates a session directly.
12
- - **Types** — `AuthenticatedCheckoutParams`, `AuthenticatedCheckoutResult`, `AnonymousCheckoutParams`
13
- - **Resources** — `CheckoutAnonymousResource`, `CheckoutAuthenticatedResource` (accessed via `client.checkout.anonymous` / `client.checkout.authenticated`)
12
+ - **Buyer self-service** — `client.buyer(token)` creates a session-token-authenticated buyer session with methods: `cancelSubscription()`, `cancelOnetimeOrder()`, `reactivateSubscription()`, `createRefundTicket()`, `resubmitRefundTicket()`, and `graphql.query()`.
13
+ - **Types** — `AuthenticatedCheckoutParams`, `AuthenticatedCheckoutResult`, `AnonymousCheckoutParams`, `CancelOnetimeOrderParams`, `CancelOnetimeOrderResult`, `ReactivateSubscriptionParams`, `ReactivateSubscriptionResult`, `CreateRefundTicketParams`, `ResubmitRefundTicketParams`, `RefundTicket`, `RequestedAmount`
14
+ - **Resources** — `CheckoutAnonymousResource`, `CheckoutAuthenticatedResource`, `BuyerSession`, `BuyerHttpClient`
14
15
 
15
16
  ### Changed
16
17
 
17
18
  - **Base URL** — Default API endpoint changed from `waffo-pancake-auth-service.vercel.app` to `api.waffo.ai`
18
19
  - **Package** — `docs/` directory now included in npm package (`files` field)
19
- - **Docs** — `docs/api-reference.md` synced with endpoint docs: fixed `storeId` required status, added `BillingDetail` conditional field rules, fixed price amount format (display string, not integer), added subscription product group UUID note
20
+ - **Docs** — `docs/api-reference.md` synced with endpoint docs: fixed `storeId` required status, added `BillingDetail` conditional field rules, fixed price amount format (display string, not integer), added subscription product group UUID note, added Buyer Self-Service section
20
21
  - **Docs** — `docs/graphql-guide.md` rewritten: corrected query names (`onetimeOrders` / `subscriptionOrders`), added product version queries, exchange rate query, webhook/email delivery log queries, 9 analytics queries (`orderStatistics`, `paymentStatistics`, `productStatistics`, `trendAnalysis`, `distributionAnalysis`, `customerAnalysis`, `taxAnalysis`, `subscriptionAnalysis`, `refundTicketAnalysis`), updated count query list and filter examples
21
22
  - **Docs** — `docs/webhook-guide.md` fixed `amount` / `taxAmount` type from `number` to `string` (display format), added retry mechanism section with delivery status table
22
- - **README** — Reorganized by use-case priority (checkout → webhooks → GraphQL → programmatic management), added checkout mode comparison and recommendation rationale
23
+ - **README** — Reorganized by use-case priority (checkout → webhooks → buyer self-service → GraphQL → programmatic management), added checkout mode comparison and recommendation rationale
23
24
 
24
25
  ## [0.1.8] - 2026-03-20
25
26
 
package/README.md CHANGED
@@ -66,32 +66,54 @@ Waffo supports two checkout modes based on whether the merchant knows the buyer'
66
66
 
67
67
  > **We recommend authenticated checkout whenever possible.** The most important reason: authenticated checkout binds the order to the `buyerIdentity` you provide, which is a **merchant-controlled stable identifier**. Even if the buyer changes the email on the checkout form, the order is still tied to the identity you specified. In anonymous mode, the buyer self-reports their email on the form — if they enter a different address, the system treats them as a new user, which means **previous orders become unlinked** and **subscription trial periods can be exploited** (a new email = a new user = a fresh trial).
68
68
  >
69
- > Anonymous checkout also uses the `shopper` role, which can **only create orders** (no cancellation, subscription management, or refund tickets) with a **1-minute single-use session**.
70
- >
71
- > | | Authenticated (`customer`) | Anonymous (`shopper`) |
69
+ > | | Authenticated | Anonymous |
72
70
  > |---|---|---|
73
71
  > | **Identity** | Merchant-provided, stable across orders | Self-reported email, may vary |
74
- > | **Permissions** | Create orders, cancel orders, manage subscriptions, submit refund tickets | Create orders **only** |
75
- > | **Session** | 5-minute TTL, auto-refreshes on each API call | 1-minute TTL, **single-use** (consumed on first API call) |
76
- > | **Subscriptions** | Fully supported — buyers can manage, cancel, or reactivate | Not practical — buyer has no session to manage the subscription afterward |
72
+ > | **Form** | Pre-filled from merchant-provided identity | Empty, buyer fills manually |
73
+ > | **Post-purchase** | Full self-service (see [Buyer Self-Service](#buyer-self-service)) | Create orders only no post-purchase self-service |
74
+ > | **Session** | 5-minute TTL, auto-refreshes | 1-minute, single-use |
75
+
76
+ Both modes support **dynamic pricing** and **trial control** at checkout time:
77
+
78
+ - `priceSnapshot` — override the product's stored price with a custom amount (e.g., coupon, volume discount)
79
+ - `withTrial` — explicitly enable or disable the trial period for subscriptions (`true` = force trial, `false` = skip trial, omit = use default rules)
77
80
 
78
81
  ### Authenticated Checkout (Recommended)
79
82
 
80
83
  The merchant provides buyer identity — the SDK issues a session token, creates a checkout session, and returns a checkout URL with the token appended as a URL fragment. One call does everything.
81
84
 
82
85
  ```typescript
86
+ // Basic — buyer identity only
87
+ const result = await client.checkout.authenticated.create({
88
+ storeId: "STO_xxx",
89
+ productId: "PROD_xxx",
90
+ productType: "onetime",
91
+ currency: "USD",
92
+ buyerIdentity: "customer@example.com",
93
+ });
94
+
95
+ // With dynamic pricing — override stored price (e.g., coupon, volume discount)
83
96
  const result = await client.checkout.authenticated.create({
84
97
  storeId: "STO_xxx",
85
98
  productId: "PROD_xxx",
86
99
  productType: "onetime",
87
100
  currency: "USD",
88
101
  buyerIdentity: "customer@example.com",
89
- // Optional: pre-fill billing details
102
+ priceSnapshot: { amount: "19.99", taxCategory: "digital_goods" },
103
+ });
104
+
105
+ // Subscription with trial control + billing detail pre-fill
106
+ const result = await client.checkout.authenticated.create({
107
+ storeId: "STO_xxx",
108
+ productId: "PROD_xxx",
109
+ productType: "subscription",
110
+ currency: "USD",
111
+ buyerIdentity: "customer@example.com",
112
+ withTrial: true, // force enable trial (false = skip, omit = default rules)
90
113
  billingDetail: { country: "US", isBusiness: false },
91
114
  });
92
- // result.checkoutUrl = "https://pancake.waffo.ai/store/{slug}/checkout/{sessionId}#token={JWT}"
93
115
 
94
- // Frontend open in a new tab (recommended)
116
+ // result.checkoutUrl = "https://pancake.waffo.ai/store/{slug}/checkout/{sessionId}#token={JWT}"
95
117
  window.open(result.checkoutUrl, "_blank", "noopener,noreferrer");
96
118
  ```
97
119
 
@@ -108,7 +130,16 @@ const result = await client.checkout.anonymous.create({
108
130
  productType: "onetime",
109
131
  currency: "USD",
110
132
  });
111
- // result.checkoutUrl = "https://pancake.waffo.ai/store/{slug}/checkout/{sessionId}"
133
+
134
+ // Also supports priceSnapshot and withTrial
135
+ const result = await client.checkout.anonymous.create({
136
+ storeId: "STO_xxx",
137
+ productId: "PROD_xxx",
138
+ productType: "subscription",
139
+ currency: "USD",
140
+ priceSnapshot: { amount: "4.99", taxCategory: "saas" },
141
+ withTrial: false, // skip trial for this session
142
+ });
112
143
 
113
144
  window.open(result.checkoutUrl, "_blank", "noopener,noreferrer");
114
145
  ```
@@ -195,6 +226,57 @@ const event = client.webhooks.verify(rawBody, sig, { environment: "prod" });
195
226
 
196
227
  See [Webhook Guide](docs/webhook-guide.md) for event types, dual-environment key architecture, key resolution chain, retry mechanism, and best practices.
197
228
 
229
+ ## Buyer Self-Service
230
+
231
+ Beyond checkout, you can let buyers manage their own orders and subscriptions — for example, embedding a "Cancel Subscription" or "Request Refund" button in your site.
232
+
233
+ Issue a session token, then use `client.buyer(token)` to get a session with self-service methods:
234
+
235
+ ```typescript
236
+ // Your backend — issue a session token for the buyer
237
+ const { token } = await client.auth.issueSessionToken({
238
+ storeId: "STO_xxx",
239
+ buyerIdentity: req.user.email,
240
+ });
241
+
242
+ // Create a buyer session
243
+ const buyer = client.buyer(token);
244
+
245
+ // Cancel a subscription
246
+ const { orderId, status } = await buyer.cancelSubscription({ orderId: "ORD_xxx" });
247
+ // status: "canceling" (active) or "canceled" (pending)
248
+
249
+ // Reactivate a canceled subscription
250
+ await buyer.reactivateSubscription({ orderId: "ORD_xxx" });
251
+
252
+ // Cancel a one-time order (while payment is pending)
253
+ await buyer.cancelOnetimeOrder({ orderId: "ORD_yyy" });
254
+
255
+ // Submit a refund request
256
+ const { ticket } = await buyer.createRefundTicket({
257
+ paymentId: "PAY_xxx",
258
+ reason: "Product not as described",
259
+ requestedAmount: { amount: "29.00", currency: "USD" },
260
+ });
261
+
262
+ // Resubmit a rejected refund ticket
263
+ await buyer.resubmitRefundTicket({
264
+ ticketId: "TKT_xxx",
265
+ paymentId: "PAY_xxx",
266
+ reason: "Updated reason with more detail",
267
+ requestedAmount: { amount: "29.00", currency: "USD" },
268
+ });
269
+
270
+ // Query the buyer's own orders via GraphQL
271
+ const result = await buyer.graphql.query({
272
+ query: `query { orders { id status createdAt } }`,
273
+ });
274
+ ```
275
+
276
+ The token is scoped to the specified store and buyer identity — buyers can only access their own data. Token TTL is 5 minutes and auto-refreshes on each API call.
277
+
278
+ > **Note**: This uses the same `buyerIdentity` as `checkout.authenticated.create()`. Orders placed via authenticated checkout are automatically tied to this identity, so the buyer can manage them later with a token issued here.
279
+
198
280
  ## GraphQL — Typed Queries
199
281
 
200
282
  ```typescript
@@ -361,8 +443,10 @@ try {
361
443
  | `client.checkout.authenticated` | `create()` | Authenticated checkout (recommended) |
362
444
  | `client.checkout.anonymous` | `create()` | Anonymous checkout |
363
445
  | `client.checkout` | `createSession()` | Low-level checkout session |
446
+ | `client.buyer(token)` | `cancelSubscription()` `cancelOnetimeOrder()` `reactivateSubscription()` `createRefundTicket()` `resubmitRefundTicket()` | Buyer self-service |
447
+ | `client.buyer(token).graphql` | `query<T>()` | Buyer-scoped GraphQL queries |
364
448
  | `client.webhooks` | `verify<T>()` | Webhook signature verification |
365
- | `client.graphql` | `query<T>()` | Typed GraphQL queries |
449
+ | `client.graphql` | `query<T>()` | Merchant GraphQL queries |
366
450
  | `client.auth` | `issueSessionToken()` | Issue a buyer session token (JWT) |
367
451
  | `client.stores` | `create()` `update()` `delete()` | Store management |
368
452
  | `client.storeMerchants` | `add()` `remove()` `updateRole()` | Store members (coming soon) |
@@ -430,7 +514,8 @@ npm run build # tsup → ESM + CJS + DTS
430
514
  src/
431
515
  ├── index.ts # Unified export entry
432
516
  ├── client.ts # WaffoPancake main class
433
- ├── http-client.ts # HTTP client (auto-signing + idempotency)
517
+ ├── http-client.ts # HTTP client (API Key, auto-signing + idempotency)
518
+ ├── buyer-http-client.ts # HTTP client (Bearer token, buyer self-service)
434
519
  ├── signing.ts # RSA-SHA256 request signing
435
520
  ├── errors.ts # WaffoPancakeError
436
521
  ├── webhooks.ts # Webhook verification (embedded keys)
@@ -443,6 +528,7 @@ src/
443
528
  ├── onetime-products.ts
444
529
  ├── subscription-products.ts
445
530
  ├── subscription-product-groups.ts
531
+ ├── buyer.ts
446
532
  ├── orders.ts
447
533
  ├── checkout.ts
448
534
  ├── checkout-anonymous.ts
package/dist/index.cjs CHANGED
@@ -41,9 +41,6 @@ __export(index_exports, {
41
41
  });
42
42
  module.exports = __toCommonJS(index_exports);
43
43
 
44
- // src/http-client.ts
45
- var import_node_crypto2 = require("crypto");
46
-
47
44
  // src/errors.ts
48
45
  var WaffoPancakeError = class extends Error {
49
46
  status;
@@ -57,6 +54,45 @@ var WaffoPancakeError = class extends Error {
57
54
  }
58
55
  };
59
56
 
57
+ // src/buyer-http-client.ts
58
+ var DEFAULT_BASE_URL = "https://api.waffo.ai";
59
+ var BuyerHttpClient = class {
60
+ token;
61
+ baseUrl;
62
+ _fetch;
63
+ constructor(token, config) {
64
+ this.token = token;
65
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
66
+ this._fetch = config.fetch ?? fetch;
67
+ }
68
+ /**
69
+ * Send a Bearer-authenticated POST request and return the parsed `data` field.
70
+ *
71
+ * @param path - API path
72
+ * @param body - Request body object
73
+ * @returns Parsed `data` field from the response
74
+ * @throws {WaffoPancakeError} When the API returns errors
75
+ */
76
+ async post(path, body) {
77
+ const response = await this._fetch(`${this.baseUrl}${path}`, {
78
+ method: "POST",
79
+ headers: {
80
+ "Content-Type": "application/json",
81
+ "Authorization": `Bearer ${this.token}`
82
+ },
83
+ body: JSON.stringify(body)
84
+ });
85
+ const result = await response.json();
86
+ if ("errors" in result && result.errors) {
87
+ throw new WaffoPancakeError(response.status, result.errors);
88
+ }
89
+ return result.data;
90
+ }
91
+ };
92
+
93
+ // src/http-client.ts
94
+ var import_node_crypto2 = require("crypto");
95
+
60
96
  // src/signing.ts
61
97
  var import_node_crypto = require("crypto");
62
98
  var PKCS8_HEADER = "-----BEGIN PRIVATE KEY-----";
@@ -169,7 +205,7 @@ ${bodyHash}`;
169
205
  }
170
206
 
171
207
  // src/http-client.ts
172
- var DEFAULT_BASE_URL = "https://api.waffo.ai";
208
+ var DEFAULT_BASE_URL2 = "https://api.waffo.ai";
173
209
  var HttpClient = class {
174
210
  merchantId;
175
211
  privateKey;
@@ -178,7 +214,7 @@ var HttpClient = class {
178
214
  constructor(config) {
179
215
  this.merchantId = config.merchantId;
180
216
  this.privateKey = normalizePrivateKey(config.privateKey);
181
- this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
217
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL2).replace(/\/+$/, "");
182
218
  this._fetch = config.fetch ?? fetch;
183
219
  }
184
220
  /**
@@ -239,6 +275,121 @@ var AuthResource = class {
239
275
  }
240
276
  };
241
277
 
278
+ // src/resources/buyer.ts
279
+ var BuyerSession = class {
280
+ constructor(http) {
281
+ this.http = http;
282
+ this.graphql = new BuyerGraphQL(http);
283
+ }
284
+ /** GraphQL query access scoped to the buyer's data. */
285
+ graphql;
286
+ /**
287
+ * Cancel a subscription order.
288
+ *
289
+ * @param params - Order to cancel
290
+ * @returns Order ID and resulting status
291
+ *
292
+ * @example
293
+ * const { orderId, status } = await buyer.cancelSubscription({ orderId: "ORD_xxx" });
294
+ * // status: "canceled" (was pending) or "canceling" (was active)
295
+ */
296
+ async cancelSubscription(params) {
297
+ return this.http.post(
298
+ "/v1/actions/subscription-order/cancel-order",
299
+ params
300
+ );
301
+ }
302
+ /**
303
+ * Cancel a one-time order (only while payment is still pending).
304
+ *
305
+ * @param params - Order to cancel
306
+ * @returns Order ID and resulting status
307
+ *
308
+ * @example
309
+ * const { orderId, status } = await buyer.cancelOnetimeOrder({ orderId: "ORD_xxx" });
310
+ */
311
+ async cancelOnetimeOrder(params) {
312
+ return this.http.post(
313
+ "/v1/actions/onetime-order/cancel-order",
314
+ params
315
+ );
316
+ }
317
+ /**
318
+ * Reactivate a subscription that is in `canceling` status.
319
+ *
320
+ * @param params - Order to reactivate
321
+ * @returns Order ID and resulting status
322
+ *
323
+ * @example
324
+ * const { orderId, status } = await buyer.reactivateSubscription({ orderId: "ORD_xxx" });
325
+ * // status: "active"
326
+ */
327
+ async reactivateSubscription(params) {
328
+ return this.http.post(
329
+ "/v1/actions/subscription-order/reactivate-order",
330
+ params
331
+ );
332
+ }
333
+ /**
334
+ * Submit a refund request for a payment.
335
+ *
336
+ * @param params - Refund ticket details
337
+ * @returns Created refund ticket
338
+ *
339
+ * @example
340
+ * const { ticket } = await buyer.createRefundTicket({
341
+ * paymentId: "PAY_xxx",
342
+ * reason: "Product not as described",
343
+ * requestedAmount: { amount: "29.00", currency: "USD" },
344
+ * });
345
+ */
346
+ async createRefundTicket(params) {
347
+ return this.http.post(
348
+ "/v1/actions/refund-ticket/create-ticket",
349
+ params
350
+ );
351
+ }
352
+ /**
353
+ * Resubmit a previously rejected refund ticket with updated details.
354
+ *
355
+ * @param params - Updated ticket details
356
+ * @returns Updated refund ticket
357
+ *
358
+ * @example
359
+ * const { ticket } = await buyer.resubmitRefundTicket({
360
+ * ticketId: "TKT_xxx",
361
+ * paymentId: "PAY_xxx",
362
+ * reason: "Updated reason with more detail",
363
+ * requestedAmount: { amount: "29.00", currency: "USD" },
364
+ * });
365
+ */
366
+ async resubmitRefundTicket(params) {
367
+ return this.http.post(
368
+ "/v1/actions/refund-ticket/resubmit-ticket",
369
+ params
370
+ );
371
+ }
372
+ };
373
+ var BuyerGraphQL = class {
374
+ constructor(http) {
375
+ this.http = http;
376
+ }
377
+ /**
378
+ * Execute a GraphQL query scoped to the buyer's data.
379
+ *
380
+ * @param params - GraphQL query and variables
381
+ * @returns GraphQL response
382
+ *
383
+ * @example
384
+ * const result = await buyer.graphql.query({
385
+ * query: `query { orders { id status } }`,
386
+ * });
387
+ */
388
+ async query(params) {
389
+ return this.http.post("/v1/graphql", params);
390
+ }
391
+ };
392
+
242
393
  // src/resources/checkout-anonymous.ts
243
394
  var CheckoutAnonymousResource = class {
244
395
  constructor(http) {
@@ -323,7 +474,7 @@ var CheckoutResource = class {
323
474
  this.anonymous = new CheckoutAnonymousResource(http);
324
475
  this.authenticated = new CheckoutAuthenticatedResource(http);
325
476
  }
326
- /** Anonymous checkout — visitor enters without a session token. */
477
+ /** Anonymous checkout — no buyer identity, empty form. */
327
478
  anonymous;
328
479
  /** Authenticated checkout — merchant provides buyer identity. */
329
480
  authenticated;
@@ -848,6 +999,7 @@ var WebhooksResource = class {
848
999
  // src/client.ts
849
1000
  var WaffoPancake = class {
850
1001
  http;
1002
+ config;
851
1003
  auth;
852
1004
  stores;
853
1005
  storeMerchants;
@@ -859,6 +1011,7 @@ var WaffoPancake = class {
859
1011
  graphql;
860
1012
  webhooks;
861
1013
  constructor(config) {
1014
+ this.config = config;
862
1015
  this.http = new HttpClient(config);
863
1016
  this.auth = new AuthResource(this.http);
864
1017
  this.stores = new StoresResource(this.http);
@@ -871,6 +1024,31 @@ var WaffoPancake = class {
871
1024
  this.graphql = new GraphQLResource(this.http);
872
1025
  this.webhooks = new WebhooksResource(config.webhookPublicKey);
873
1026
  }
1027
+ /**
1028
+ * Create a buyer session for self-service operations.
1029
+ *
1030
+ * The returned session uses Bearer token authentication and provides
1031
+ * methods for order cancellation, subscription management, refund tickets,
1032
+ * and scoped GraphQL queries.
1033
+ *
1034
+ * @param token - Session token from `client.auth.issueSessionToken()`
1035
+ * @returns A buyer session with self-service methods
1036
+ *
1037
+ * @example
1038
+ * const { token } = await client.auth.issueSessionToken({
1039
+ * storeId: "STO_xxx",
1040
+ * buyerIdentity: "customer@example.com",
1041
+ * });
1042
+ * const buyer = client.buyer(token);
1043
+ * await buyer.cancelSubscription({ orderId: "ORD_xxx" });
1044
+ */
1045
+ buyer(token) {
1046
+ const buyerHttp = new BuyerHttpClient(token, {
1047
+ baseUrl: this.config.baseUrl,
1048
+ fetch: this.config.fetch
1049
+ });
1050
+ return new BuyerSession(buyerHttp);
1051
+ }
874
1052
  };
875
1053
 
876
1054
  // src/types.ts