@stardeck-customer-apps/testing 0.27.0 → 0.29.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/SKILL.md +61 -2
- package/dist/index.d.mts +122 -1
- package/dist/index.d.ts +122 -1
- package/dist/index.js +737 -6
- package/dist/index.mjs +737 -6
- package/dist/next/headers-shim.js +14 -0
- package/dist/next/headers-shim.mjs +14 -0
- package/dist/setup.js +479 -4
- package/dist/setup.mjs +479 -4
- package/package.json +6 -1
package/SKILL.md
CHANGED
|
@@ -233,6 +233,21 @@ app.payments.setStripePaymentMethods(["card"]); // merchant turned PromptPay of
|
|
|
233
233
|
await expect(payments.createStripePaymentIntent({ ...req, method: "promptpay" }))
|
|
234
234
|
.rejects.toMatchObject({ statusCode: 422, code: "method_not_allowed" });
|
|
235
235
|
|
|
236
|
+
// Provider refunds (payments-sdk >= 4.13.0): same routes, summaries and error codes as the
|
|
237
|
+
// platform. A refund never pays out on its own; you move it, then deliver the event, then the
|
|
238
|
+
// app re-reads the refund (as it must in production).
|
|
239
|
+
const refund = await payments.refundStripePaymentIntent(pi.id, {
|
|
240
|
+
idempotencyKey: row.id, amount: 5000, refundRowId: row.id,
|
|
241
|
+
}); // status "pending"; app.payments.getStripePaymentIntent(pi.id).amountRefunded === 5000
|
|
242
|
+
app.payments.settleRefund(refund.id, "failed"); // or "succeeded" / "needs_customer"
|
|
243
|
+
await app.payments.deliverStripeEvent(POST, { refundId: refund.id }); // refund.failed
|
|
244
|
+
// amountRefunded drops back to 0 and the payment can be refunded again.
|
|
245
|
+
// Staff refunded in Stripe's dashboard: already succeeded, no refundRowId. It returns the new
|
|
246
|
+
// refund's id; deliver it with deliverStripeEvent(POST, { refundId: dashId }) the same way.
|
|
247
|
+
const dashId = app.payments.dashboardRefund(pi.id, 3000);
|
|
248
|
+
// Beam: refundBeamCharge(chargeId, { idempotencyKey, amount? }) -> PENDING; settleRefund(id,
|
|
249
|
+
// "succeeded" | "failed"); deliverBeamEvent(POST, { refundId }) -> refund.succeeded / refund.failed.
|
|
250
|
+
|
|
236
251
|
// File upload
|
|
237
252
|
const storage = new StorageClient();
|
|
238
253
|
await storage.upload(new File(["hi"], "note.txt", { type: "text/plain" }));
|
|
@@ -467,6 +482,31 @@ externalId })` helper models a trusted platform/channel link, and
|
|
|
467
482
|
`already_succeeded` on cancel after payment), and another project's payment id → 404
|
|
468
483
|
`not_found`. The app's `POST /api/payments/payment-intents` route refuses card (403
|
|
469
484
|
`CARD_CHARGE_SERVER_ONLY`); create card payments from server code.
|
|
485
|
+
Provider refunds (payments-sdk >= 4.13.0): `refundStripePaymentIntent`, `getStripeRefund`,
|
|
486
|
+
`findStripeCheckoutSessionByPaymentIntent`, `refundBeamCharge` and `getBeamRefund` answer
|
|
487
|
+
like the platform (400 `invalid_request`; 404 `payment_not_owned` / `refund_not_found`; 409
|
|
488
|
+
`payment_not_refundable`, `refund_exceeds_remaining`, `refund_partial_not_allowed`,
|
|
489
|
+
`refund_in_progress`, `idempotency_conflict`; a repeated `idempotencyKey` returns the same
|
|
490
|
+
refund). Refunds start Stripe `pending` / Beam `PENDING` and never pay out by themselves:
|
|
491
|
+
`.settleRefund(refundId, "succeeded" | "failed" | "needs_customer", { failureReason? })`
|
|
492
|
+
moves one (`needs_customer` is Stripe PromptPay's `requires_action` with
|
|
493
|
+
`nextActionExpiresAt`, and throws on a card refund or on Beam; a succeeded Stripe refund can
|
|
494
|
+
still be settled `failed`, as a card refund can fail days later; Beam statuses are final).
|
|
495
|
+
`.dashboardRefund(paymentRef, amount?)` makes a refund the app never requested (a Stripe
|
|
496
|
+
PaymentIntent id or a Beam direct-charge id): already succeeded, no `refundRowId`; returns the new refund's id.
|
|
497
|
+
Neither delivers a webhook: follow with `.deliverStripeEvent(handler, { refundId })`
|
|
498
|
+
(`refund.created` for a new refund, `refund.updated` after a settle, `refund.failed` once
|
|
499
|
+
failed; pass `type` to force one, e.g. to test events arriving out of order) or
|
|
500
|
+
`.deliverBeamEvent(handler, { refundId })` (`refund.succeeded` / `refund.failed`; a PENDING
|
|
501
|
+
refund has no event). The event carries the refund's id, so handlers re-read it with
|
|
502
|
+
`getStripeRefund` / `getBeamRefund`. Failed refunds stop counting: the intent's
|
|
503
|
+
`amountRefunded` (succeeded + pending Stripe refunds) drops back and the amount can be
|
|
504
|
+
refunded again. Beam follows its rules: partial only on `CARD` / `CARD_TOKEN` charges, one
|
|
505
|
+
PENDING refund per charge. Inspect with `.stripeRefunds`, `.getStripeRefund(id)`,
|
|
506
|
+
`.beamRefunds`, `.getBeamRefund(id)`. A paid one-time Stripe Checkout session (`markPaid`)
|
|
507
|
+
gets a succeeded PaymentIntent: `getCheckoutSessionStatus` returns its `paymentIntentId`
|
|
508
|
+
(null before payment) and it can be refunded. Refund calls are server-only; the app's
|
|
509
|
+
payments route does not expose them.
|
|
470
510
|
Bolt+ controls: `.approveBoltIntent(id)`, `.declineBoltIntent(id)`,
|
|
471
511
|
`.expireBoltIntent(id)`, `.listBoltIntents()`, `.getBoltIntent(id)`,
|
|
472
512
|
`.count`, `.clear()`. Approve marks PAID and records a charge; deliver the
|
|
@@ -572,7 +612,7 @@ true })` remains source-compatible but the simulator (like the control plane)
|
|
|
572
612
|
the returned canonical set. Page directory UIs through `search({ query,
|
|
573
613
|
cursor, limit })`; `list()` intentionally keeps its previous array shape.
|
|
574
614
|
- `PaymentsServerClient` (Stripe checkout + Stripe inline payments + Beam payment links + Beam direct
|
|
575
|
-
charges and card tokenization + Bolt+ connections/intents/charges, product list) — captured in `app.payments`;
|
|
615
|
+
charges and card tokenization + Bolt+ connections/intents/charges, product list, provider refunds) — captured in `app.payments`;
|
|
576
616
|
fulfill checkout via `markPaid` (poll) or `deliverStripeEvent` /
|
|
577
617
|
`deliverBeamEvent` (webhook push). Drive a terminal without hardware via
|
|
578
618
|
`approveBoltIntent` / `declineBoltIntent` / `expireBoltIntent`. Beam payment-link
|
|
@@ -588,8 +628,9 @@ cursor, limit })`; `list()` intentionally keeps its previous array shape.
|
|
|
588
628
|
`stripe.createCheckout`, `stripe.getCheckoutSessionStatus`,
|
|
589
629
|
`stripe.createPaymentIntent`, `stripe.getPaymentIntent`, `stripe.findPaymentIntents`,
|
|
590
630
|
`stripe.cancelPaymentIntent`, `stripe.getElementsConfig`, `stripe.getReadiness`,
|
|
631
|
+
`stripe.refundPaymentIntent`, `stripe.getRefund`, `stripe.findCheckoutSessionByIntent`,
|
|
591
632
|
`beam.createPaymentLink`, `beam.getPaymentLinkStatus`, `beam.createDirectCharge`,
|
|
592
|
-
and `beam.
|
|
633
|
+
`beam.getCharge`, `beam.refundCharge`, and `beam.getRefund`. HTTP failures support
|
|
593
634
|
statuses 400, 402, 409, 429, 500, and 503 plus optional `code`, `details`, and
|
|
594
635
|
`retryAfterSeconds`; `{ kind: "network", message }` rejects fetch instead.
|
|
595
636
|
`app.reset()` clears pending failures.
|
|
@@ -624,6 +665,24 @@ cursor, limit })`; `list()` intentionally keeps its previous array shape.
|
|
|
624
665
|
`expect(app.schedules.earliestWake("booking-janitor")!.getTime()).toBeLessThanOrEqual(dueAt.getTime())`.
|
|
625
666
|
`app.schedules.wakesFor(key)` lists them; `reset()` clears them. Run the job's handler
|
|
626
667
|
yourself to test what it does and the `nextRun()` it returns.
|
|
668
|
+
- `getControl` / `getControls` / `setControl` from `@stardeck-customer-apps/controls/server` — the
|
|
669
|
+
sync (`POST /api/deployments/:id/controls`) and write (`PUT .../controls/:key`) routes are
|
|
670
|
+
simulated, signature-checked like the others, and served from `app.controls`. Defaults are
|
|
671
|
+
permissive so existing tests keep passing: `flag.paymentsSetupStep: false`,
|
|
672
|
+
`plan.locations: { included: 3, max: null, extraMonthly: { amount: 990, currency: "THB" } }`,
|
|
673
|
+
`plan.multiEntity: true`, `payments.provider: "beam"`, `payments.methods: { card: true, promptPay: true }`.
|
|
674
|
+
Seed with `createTestApp({ controls: { "plan.multiEntity": false } })`. Then
|
|
675
|
+
`app.controls.set(key, value)` (the next `getControl` in the same process sees it: the
|
|
676
|
+
controls cache is dropped on every change and on `reset()`),
|
|
677
|
+
`app.controls.setUnavailable(key)` (that key reads `{ ok: false }`, like a platform outage),
|
|
678
|
+
`app.controls.lastUsage()` (the usage the app reported on its latest sync, for example
|
|
679
|
+
`{ locations: 4 }`), `app.controls.writes()` (each `setControl` the app made:
|
|
680
|
+
`{ key, value, userToken }`) and `app.controls.refuseNextWrite({ code, message, connectUrl? })`
|
|
681
|
+
(the next write fails once with that error, for example `PROVIDER_NOT_READY` with a connect
|
|
682
|
+
link). A write needs the person's session token in `X-Stardeck-User-Token`; without it the
|
|
683
|
+
simulator answers 401 `UNAUTHENTICATED`. Only `payments.provider` and `payments.methods` are
|
|
684
|
+
writable (anything else is 400 `NOT_WRITABLE`), and an invalid value is 400 `INVALID_VALUE`.
|
|
685
|
+
`app.controls.clear()` returns to the seeded values. Test the `ok: false` path of every gate.
|
|
627
686
|
|
|
628
687
|
### Receipt layout and printer capabilities
|
|
629
688
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as _stardeck_customer_apps_payments_sdk from '@stardeck-customer-apps/payments-sdk';
|
|
2
2
|
import * as next_server from 'next/server';
|
|
3
|
+
import { ControlKey, ControlValue, ControlsUsage, ControlWriteErrorCode } from '@stardeck-customer-apps/controls';
|
|
3
4
|
import { PGlite } from '@electric-sql/pglite';
|
|
4
5
|
import { T as TestDataStoreInput } from './data-store-manifest-Zwgcv0DJ.mjs';
|
|
5
6
|
export { S as STARDECK_DATA_STORES_ENV, a as TestDataStoreManifestEntry, b as buildTestDataStoreManifest } from './data-store-manifest-Zwgcv0DJ.mjs';
|
|
@@ -216,7 +217,7 @@ interface CapturedCheckout {
|
|
|
216
217
|
metadata?: Record<string, string>;
|
|
217
218
|
createdAt: Date;
|
|
218
219
|
}
|
|
219
|
-
type PaymentSimulatorOperation = "stripe.createCheckout" | "stripe.getCheckoutSessionStatus" | "beam.createPaymentLink" | "beam.getPaymentLinkStatus" | "beam.createDirectCharge" | "beam.getCharge" | "stripe.createPaymentIntent" | "stripe.getPaymentIntent" | "stripe.findPaymentIntents" | "stripe.cancelPaymentIntent" | "stripe.getElementsConfig" | "stripe.getReadiness";
|
|
220
|
+
type PaymentSimulatorOperation = "stripe.createCheckout" | "stripe.getCheckoutSessionStatus" | "beam.createPaymentLink" | "beam.getPaymentLinkStatus" | "beam.createDirectCharge" | "beam.getCharge" | "stripe.createPaymentIntent" | "stripe.getPaymentIntent" | "stripe.findPaymentIntents" | "stripe.cancelPaymentIntent" | "stripe.getElementsConfig" | "stripe.getReadiness" | "stripe.refundPaymentIntent" | "stripe.getRefund" | "stripe.findCheckoutSessionByIntent" | "beam.refundCharge" | "beam.getRefund";
|
|
220
221
|
type PaymentSimulatorFailure = {
|
|
221
222
|
kind?: "http";
|
|
222
223
|
status: 400 | 402 | 404 | 409 | 422 | 429 | 500 | 502 | 503;
|
|
@@ -338,6 +339,46 @@ interface SimStripePaymentIntent {
|
|
|
338
339
|
/** Unix seconds. */
|
|
339
340
|
created: number;
|
|
340
341
|
}
|
|
342
|
+
/** A Stripe refund as the simulator holds it. */
|
|
343
|
+
interface SimStripeRefund {
|
|
344
|
+
id: string;
|
|
345
|
+
paymentIntentId: string;
|
|
346
|
+
/** The project that owns the payment; other projects get 404. */
|
|
347
|
+
projectId: string;
|
|
348
|
+
amount: number;
|
|
349
|
+
/** Lowercase. */
|
|
350
|
+
currency: string;
|
|
351
|
+
status: _stardeck_customer_apps_payments_sdk.StripeRefundStatus;
|
|
352
|
+
failureReason: string | null;
|
|
353
|
+
pendingReason: string | null;
|
|
354
|
+
/** Unix seconds; set while a PromptPay refund waits for the customer's bank details. */
|
|
355
|
+
nextActionExpiresAt: number | null;
|
|
356
|
+
/** The app's local refund row id; null for a refund made in the Stripe dashboard. */
|
|
357
|
+
refundRowId: string | null;
|
|
358
|
+
/** The app's metadata, with Stardeck's reserved keys removed. */
|
|
359
|
+
appMetadata: Record<string, string>;
|
|
360
|
+
reason: string | null;
|
|
361
|
+
/** "app" when requested through the SDK, "dashboard" when made by `dashboardRefund`. */
|
|
362
|
+
origin: "app" | "dashboard";
|
|
363
|
+
/** How many times `settleRefund` has moved it; picks `refund.created` vs `refund.updated`. */
|
|
364
|
+
revision: number;
|
|
365
|
+
/** Unix seconds. */
|
|
366
|
+
created: number;
|
|
367
|
+
}
|
|
368
|
+
/** A Beam refund as the simulator holds it. */
|
|
369
|
+
interface SimBeamRefund {
|
|
370
|
+
refundId: string;
|
|
371
|
+
chargeId: string;
|
|
372
|
+
amount: number;
|
|
373
|
+
/** As Beam returns it, e.g. "THB". */
|
|
374
|
+
currency: string;
|
|
375
|
+
status: _stardeck_customer_apps_payments_sdk.BeamRefundStatus;
|
|
376
|
+
failureCode: string | null;
|
|
377
|
+
reason: string | null;
|
|
378
|
+
origin: "app" | "dashboard";
|
|
379
|
+
/** ISO 8601. */
|
|
380
|
+
createdAt: string;
|
|
381
|
+
}
|
|
341
382
|
interface TestPayments {
|
|
342
383
|
get checkouts(): CapturedCheckout[];
|
|
343
384
|
latest(): CapturedCheckout | undefined;
|
|
@@ -387,17 +428,62 @@ interface TestPayments {
|
|
|
387
428
|
accountId?: string;
|
|
388
429
|
} | {
|
|
389
430
|
paymentIntentId: string;
|
|
431
|
+
} | {
|
|
432
|
+
refundId: string;
|
|
433
|
+
type?: "refund.created" | "refund.updated" | "refund.failed";
|
|
390
434
|
}, options?: {
|
|
391
435
|
path?: string;
|
|
392
436
|
deploymentSecret?: string;
|
|
393
437
|
}): Promise<Response>;
|
|
438
|
+
/**
|
|
439
|
+
* Deliver a Beam webhook. Pass `{ refundId }` to send the `refund.succeeded`
|
|
440
|
+
* or `refund.failed` event for a settled Beam refund, built from the stored
|
|
441
|
+
* refund. A refund still PENDING has no event: call `settleRefund` first.
|
|
442
|
+
*/
|
|
394
443
|
deliverBeamEvent(handler: PaymentsWebhookHandler, event: {
|
|
395
444
|
type: string;
|
|
396
445
|
payload: Record<string, unknown>;
|
|
446
|
+
} | {
|
|
447
|
+
refundId: string;
|
|
397
448
|
}, options?: {
|
|
398
449
|
path?: string;
|
|
399
450
|
deploymentSecret?: string;
|
|
400
451
|
}): Promise<Response>;
|
|
452
|
+
/** Every Stripe refund so far, oldest first, as stored. */
|
|
453
|
+
get stripeRefunds(): SimStripeRefund[];
|
|
454
|
+
getStripeRefund(refundId: string): SimStripeRefund | undefined;
|
|
455
|
+
/** Every Beam refund so far, oldest first, as stored. */
|
|
456
|
+
get beamRefunds(): SimBeamRefund[];
|
|
457
|
+
getBeamRefund(refundId: string): SimBeamRefund | undefined;
|
|
458
|
+
/**
|
|
459
|
+
* Move a refund the app requested to its next state, as the provider would
|
|
460
|
+
* later. Refunds start `pending` (Stripe PromptPay: `requires_action`) and
|
|
461
|
+
* never pay out on their own.
|
|
462
|
+
*
|
|
463
|
+
* - `"succeeded"`: Stripe `succeeded` / Beam `SUCCEEDED`.
|
|
464
|
+
* - `"failed"`: Stripe `failed` (also from `succeeded`, like a card refund
|
|
465
|
+
* that fails days later) / Beam `FAILED`. A failed refund stops counting
|
|
466
|
+
* toward `amountRefunded` and toward what the payment can still refund.
|
|
467
|
+
* - `"needs_customer"`: Stripe PromptPay only (`requires_action`: the
|
|
468
|
+
* customer must send bank details). Beam has no such state and throws.
|
|
469
|
+
*
|
|
470
|
+
* `failureReason` is Stripe's `failure_reason` or Beam's `failureCode`.
|
|
471
|
+
* Delivers no webhook: follow with `deliverStripeEvent(handler, { refundId })`
|
|
472
|
+
* or `deliverBeamEvent(handler, { refundId })`. Read the result back with
|
|
473
|
+
* `getStripeRefund` / `getBeamRefund` or the SDK.
|
|
474
|
+
*/
|
|
475
|
+
settleRefund(refundId: string, outcome: "succeeded" | "failed" | "needs_customer", options?: {
|
|
476
|
+
failureReason?: string;
|
|
477
|
+
}): void;
|
|
478
|
+
/**
|
|
479
|
+
* A refund made in the provider's dashboard, which the app never requested:
|
|
480
|
+
* no `refundRowId`, already succeeded. `paymentRef` is a Stripe PaymentIntent
|
|
481
|
+
* id or a Beam charge id; omit `amount` to refund what remains. The provider's
|
|
482
|
+
* own rules apply (Beam: partial on CARD charges only). Delivers no webhook;
|
|
483
|
+
* follow with `deliverStripeEvent` / `deliverBeamEvent` and `{ refundId }`.
|
|
484
|
+
* Returns the new refund's id.
|
|
485
|
+
*/
|
|
486
|
+
dashboardRefund(paymentRef: string, amount?: number): string;
|
|
401
487
|
/**
|
|
402
488
|
* Mark a PENDING bolt intent PAID and record a SUCCEEDED charge. Does not
|
|
403
489
|
* deliver a webhook — call `deliverBeamEvent` with `charge.succeeded` (reuse
|
|
@@ -745,7 +831,40 @@ interface TestSchedules {
|
|
|
745
831
|
earliestWake(scheduleKey: string): Date | undefined;
|
|
746
832
|
clear(): void;
|
|
747
833
|
}
|
|
834
|
+
interface CapturedControlWrite {
|
|
835
|
+
key: ControlKey;
|
|
836
|
+
value: unknown;
|
|
837
|
+
/** The person's session token the app sent in `X-Stardeck-User-Token`. */
|
|
838
|
+
userToken: string;
|
|
839
|
+
}
|
|
840
|
+
interface ControlRefusal {
|
|
841
|
+
code: ControlWriteErrorCode;
|
|
842
|
+
message: string;
|
|
843
|
+
connectUrl?: string;
|
|
844
|
+
}
|
|
845
|
+
/** The simulated app-controls sync and write routes (`@stardeck-customer-apps/controls`). */
|
|
846
|
+
interface TestControls {
|
|
847
|
+
/**
|
|
848
|
+
* Set a control's value (the app sees it on its next `getControl`, with no
|
|
849
|
+
* stale cache). Also makes a key that was forced unavailable available again.
|
|
850
|
+
*/
|
|
851
|
+
set<K extends ControlKey>(key: K, value: ControlValue<K>): void;
|
|
852
|
+
/** Make the sync report this control as `{ ok: false }`, like a platform outage for one key. */
|
|
853
|
+
setUnavailable(key: ControlKey): void;
|
|
854
|
+
/** The usage the app reported on its latest sync, or null if it reported none. */
|
|
855
|
+
lastUsage(): ControlsUsage | null;
|
|
856
|
+
/** Writes the app made through `setControl`, oldest first. */
|
|
857
|
+
writes(): CapturedControlWrite[];
|
|
858
|
+
/** The next write is refused with this error, then writes succeed again. */
|
|
859
|
+
refuseNextWrite(refusal: ControlRefusal): void;
|
|
860
|
+
/** Back to the values the test app was created with; clears writes, usage and refusals. */
|
|
861
|
+
clear(): void;
|
|
862
|
+
}
|
|
748
863
|
interface TestAppOptions {
|
|
864
|
+
/** Control values the simulated platform serves; unlisted keys keep the permissive defaults. */
|
|
865
|
+
controls?: Partial<{
|
|
866
|
+
[K in ControlKey]: ControlValue<K>;
|
|
867
|
+
}>;
|
|
749
868
|
/**
|
|
750
869
|
* The LIFF ID the platform forwards to the app for a LINE MINI App shop. It is
|
|
751
870
|
* what `GET /api/auth/me` returns as `liffId` and what makes `useLiff()` start
|
|
@@ -799,6 +918,8 @@ interface TestApp {
|
|
|
799
918
|
starlens: TestStarlens;
|
|
800
919
|
/** Wakes the app asked of its adaptive schedules (`wakeSchedule`). */
|
|
801
920
|
schedules: TestSchedules;
|
|
921
|
+
/** Platform-owned app controls: flags, plan limits, payment provider and methods. */
|
|
922
|
+
controls: TestControls;
|
|
802
923
|
/** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
|
|
803
924
|
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
804
925
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as _stardeck_customer_apps_payments_sdk from '@stardeck-customer-apps/payments-sdk';
|
|
2
2
|
import * as next_server from 'next/server';
|
|
3
|
+
import { ControlKey, ControlValue, ControlsUsage, ControlWriteErrorCode } from '@stardeck-customer-apps/controls';
|
|
3
4
|
import { PGlite } from '@electric-sql/pglite';
|
|
4
5
|
import { T as TestDataStoreInput } from './data-store-manifest-Zwgcv0DJ.js';
|
|
5
6
|
export { S as STARDECK_DATA_STORES_ENV, a as TestDataStoreManifestEntry, b as buildTestDataStoreManifest } from './data-store-manifest-Zwgcv0DJ.js';
|
|
@@ -216,7 +217,7 @@ interface CapturedCheckout {
|
|
|
216
217
|
metadata?: Record<string, string>;
|
|
217
218
|
createdAt: Date;
|
|
218
219
|
}
|
|
219
|
-
type PaymentSimulatorOperation = "stripe.createCheckout" | "stripe.getCheckoutSessionStatus" | "beam.createPaymentLink" | "beam.getPaymentLinkStatus" | "beam.createDirectCharge" | "beam.getCharge" | "stripe.createPaymentIntent" | "stripe.getPaymentIntent" | "stripe.findPaymentIntents" | "stripe.cancelPaymentIntent" | "stripe.getElementsConfig" | "stripe.getReadiness";
|
|
220
|
+
type PaymentSimulatorOperation = "stripe.createCheckout" | "stripe.getCheckoutSessionStatus" | "beam.createPaymentLink" | "beam.getPaymentLinkStatus" | "beam.createDirectCharge" | "beam.getCharge" | "stripe.createPaymentIntent" | "stripe.getPaymentIntent" | "stripe.findPaymentIntents" | "stripe.cancelPaymentIntent" | "stripe.getElementsConfig" | "stripe.getReadiness" | "stripe.refundPaymentIntent" | "stripe.getRefund" | "stripe.findCheckoutSessionByIntent" | "beam.refundCharge" | "beam.getRefund";
|
|
220
221
|
type PaymentSimulatorFailure = {
|
|
221
222
|
kind?: "http";
|
|
222
223
|
status: 400 | 402 | 404 | 409 | 422 | 429 | 500 | 502 | 503;
|
|
@@ -338,6 +339,46 @@ interface SimStripePaymentIntent {
|
|
|
338
339
|
/** Unix seconds. */
|
|
339
340
|
created: number;
|
|
340
341
|
}
|
|
342
|
+
/** A Stripe refund as the simulator holds it. */
|
|
343
|
+
interface SimStripeRefund {
|
|
344
|
+
id: string;
|
|
345
|
+
paymentIntentId: string;
|
|
346
|
+
/** The project that owns the payment; other projects get 404. */
|
|
347
|
+
projectId: string;
|
|
348
|
+
amount: number;
|
|
349
|
+
/** Lowercase. */
|
|
350
|
+
currency: string;
|
|
351
|
+
status: _stardeck_customer_apps_payments_sdk.StripeRefundStatus;
|
|
352
|
+
failureReason: string | null;
|
|
353
|
+
pendingReason: string | null;
|
|
354
|
+
/** Unix seconds; set while a PromptPay refund waits for the customer's bank details. */
|
|
355
|
+
nextActionExpiresAt: number | null;
|
|
356
|
+
/** The app's local refund row id; null for a refund made in the Stripe dashboard. */
|
|
357
|
+
refundRowId: string | null;
|
|
358
|
+
/** The app's metadata, with Stardeck's reserved keys removed. */
|
|
359
|
+
appMetadata: Record<string, string>;
|
|
360
|
+
reason: string | null;
|
|
361
|
+
/** "app" when requested through the SDK, "dashboard" when made by `dashboardRefund`. */
|
|
362
|
+
origin: "app" | "dashboard";
|
|
363
|
+
/** How many times `settleRefund` has moved it; picks `refund.created` vs `refund.updated`. */
|
|
364
|
+
revision: number;
|
|
365
|
+
/** Unix seconds. */
|
|
366
|
+
created: number;
|
|
367
|
+
}
|
|
368
|
+
/** A Beam refund as the simulator holds it. */
|
|
369
|
+
interface SimBeamRefund {
|
|
370
|
+
refundId: string;
|
|
371
|
+
chargeId: string;
|
|
372
|
+
amount: number;
|
|
373
|
+
/** As Beam returns it, e.g. "THB". */
|
|
374
|
+
currency: string;
|
|
375
|
+
status: _stardeck_customer_apps_payments_sdk.BeamRefundStatus;
|
|
376
|
+
failureCode: string | null;
|
|
377
|
+
reason: string | null;
|
|
378
|
+
origin: "app" | "dashboard";
|
|
379
|
+
/** ISO 8601. */
|
|
380
|
+
createdAt: string;
|
|
381
|
+
}
|
|
341
382
|
interface TestPayments {
|
|
342
383
|
get checkouts(): CapturedCheckout[];
|
|
343
384
|
latest(): CapturedCheckout | undefined;
|
|
@@ -387,17 +428,62 @@ interface TestPayments {
|
|
|
387
428
|
accountId?: string;
|
|
388
429
|
} | {
|
|
389
430
|
paymentIntentId: string;
|
|
431
|
+
} | {
|
|
432
|
+
refundId: string;
|
|
433
|
+
type?: "refund.created" | "refund.updated" | "refund.failed";
|
|
390
434
|
}, options?: {
|
|
391
435
|
path?: string;
|
|
392
436
|
deploymentSecret?: string;
|
|
393
437
|
}): Promise<Response>;
|
|
438
|
+
/**
|
|
439
|
+
* Deliver a Beam webhook. Pass `{ refundId }` to send the `refund.succeeded`
|
|
440
|
+
* or `refund.failed` event for a settled Beam refund, built from the stored
|
|
441
|
+
* refund. A refund still PENDING has no event: call `settleRefund` first.
|
|
442
|
+
*/
|
|
394
443
|
deliverBeamEvent(handler: PaymentsWebhookHandler, event: {
|
|
395
444
|
type: string;
|
|
396
445
|
payload: Record<string, unknown>;
|
|
446
|
+
} | {
|
|
447
|
+
refundId: string;
|
|
397
448
|
}, options?: {
|
|
398
449
|
path?: string;
|
|
399
450
|
deploymentSecret?: string;
|
|
400
451
|
}): Promise<Response>;
|
|
452
|
+
/** Every Stripe refund so far, oldest first, as stored. */
|
|
453
|
+
get stripeRefunds(): SimStripeRefund[];
|
|
454
|
+
getStripeRefund(refundId: string): SimStripeRefund | undefined;
|
|
455
|
+
/** Every Beam refund so far, oldest first, as stored. */
|
|
456
|
+
get beamRefunds(): SimBeamRefund[];
|
|
457
|
+
getBeamRefund(refundId: string): SimBeamRefund | undefined;
|
|
458
|
+
/**
|
|
459
|
+
* Move a refund the app requested to its next state, as the provider would
|
|
460
|
+
* later. Refunds start `pending` (Stripe PromptPay: `requires_action`) and
|
|
461
|
+
* never pay out on their own.
|
|
462
|
+
*
|
|
463
|
+
* - `"succeeded"`: Stripe `succeeded` / Beam `SUCCEEDED`.
|
|
464
|
+
* - `"failed"`: Stripe `failed` (also from `succeeded`, like a card refund
|
|
465
|
+
* that fails days later) / Beam `FAILED`. A failed refund stops counting
|
|
466
|
+
* toward `amountRefunded` and toward what the payment can still refund.
|
|
467
|
+
* - `"needs_customer"`: Stripe PromptPay only (`requires_action`: the
|
|
468
|
+
* customer must send bank details). Beam has no such state and throws.
|
|
469
|
+
*
|
|
470
|
+
* `failureReason` is Stripe's `failure_reason` or Beam's `failureCode`.
|
|
471
|
+
* Delivers no webhook: follow with `deliverStripeEvent(handler, { refundId })`
|
|
472
|
+
* or `deliverBeamEvent(handler, { refundId })`. Read the result back with
|
|
473
|
+
* `getStripeRefund` / `getBeamRefund` or the SDK.
|
|
474
|
+
*/
|
|
475
|
+
settleRefund(refundId: string, outcome: "succeeded" | "failed" | "needs_customer", options?: {
|
|
476
|
+
failureReason?: string;
|
|
477
|
+
}): void;
|
|
478
|
+
/**
|
|
479
|
+
* A refund made in the provider's dashboard, which the app never requested:
|
|
480
|
+
* no `refundRowId`, already succeeded. `paymentRef` is a Stripe PaymentIntent
|
|
481
|
+
* id or a Beam charge id; omit `amount` to refund what remains. The provider's
|
|
482
|
+
* own rules apply (Beam: partial on CARD charges only). Delivers no webhook;
|
|
483
|
+
* follow with `deliverStripeEvent` / `deliverBeamEvent` and `{ refundId }`.
|
|
484
|
+
* Returns the new refund's id.
|
|
485
|
+
*/
|
|
486
|
+
dashboardRefund(paymentRef: string, amount?: number): string;
|
|
401
487
|
/**
|
|
402
488
|
* Mark a PENDING bolt intent PAID and record a SUCCEEDED charge. Does not
|
|
403
489
|
* deliver a webhook — call `deliverBeamEvent` with `charge.succeeded` (reuse
|
|
@@ -745,7 +831,40 @@ interface TestSchedules {
|
|
|
745
831
|
earliestWake(scheduleKey: string): Date | undefined;
|
|
746
832
|
clear(): void;
|
|
747
833
|
}
|
|
834
|
+
interface CapturedControlWrite {
|
|
835
|
+
key: ControlKey;
|
|
836
|
+
value: unknown;
|
|
837
|
+
/** The person's session token the app sent in `X-Stardeck-User-Token`. */
|
|
838
|
+
userToken: string;
|
|
839
|
+
}
|
|
840
|
+
interface ControlRefusal {
|
|
841
|
+
code: ControlWriteErrorCode;
|
|
842
|
+
message: string;
|
|
843
|
+
connectUrl?: string;
|
|
844
|
+
}
|
|
845
|
+
/** The simulated app-controls sync and write routes (`@stardeck-customer-apps/controls`). */
|
|
846
|
+
interface TestControls {
|
|
847
|
+
/**
|
|
848
|
+
* Set a control's value (the app sees it on its next `getControl`, with no
|
|
849
|
+
* stale cache). Also makes a key that was forced unavailable available again.
|
|
850
|
+
*/
|
|
851
|
+
set<K extends ControlKey>(key: K, value: ControlValue<K>): void;
|
|
852
|
+
/** Make the sync report this control as `{ ok: false }`, like a platform outage for one key. */
|
|
853
|
+
setUnavailable(key: ControlKey): void;
|
|
854
|
+
/** The usage the app reported on its latest sync, or null if it reported none. */
|
|
855
|
+
lastUsage(): ControlsUsage | null;
|
|
856
|
+
/** Writes the app made through `setControl`, oldest first. */
|
|
857
|
+
writes(): CapturedControlWrite[];
|
|
858
|
+
/** The next write is refused with this error, then writes succeed again. */
|
|
859
|
+
refuseNextWrite(refusal: ControlRefusal): void;
|
|
860
|
+
/** Back to the values the test app was created with; clears writes, usage and refusals. */
|
|
861
|
+
clear(): void;
|
|
862
|
+
}
|
|
748
863
|
interface TestAppOptions {
|
|
864
|
+
/** Control values the simulated platform serves; unlisted keys keep the permissive defaults. */
|
|
865
|
+
controls?: Partial<{
|
|
866
|
+
[K in ControlKey]: ControlValue<K>;
|
|
867
|
+
}>;
|
|
749
868
|
/**
|
|
750
869
|
* The LIFF ID the platform forwards to the app for a LINE MINI App shop. It is
|
|
751
870
|
* what `GET /api/auth/me` returns as `liffId` and what makes `useLiff()` start
|
|
@@ -799,6 +918,8 @@ interface TestApp {
|
|
|
799
918
|
starlens: TestStarlens;
|
|
800
919
|
/** Wakes the app asked of its adaptive schedules (`wakeSchedule`). */
|
|
801
920
|
schedules: TestSchedules;
|
|
921
|
+
/** Platform-owned app controls: flags, plan limits, payment provider and methods. */
|
|
922
|
+
controls: TestControls;
|
|
802
923
|
/** Convenience for raw SQL: `app.query("SELECT ...", [param])`. */
|
|
803
924
|
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
804
925
|
/**
|