@vivoa/partner-sdk 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.
Files changed (106) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +162 -0
  3. package/dist/cjs/adapters/fetch-transport.d.ts +7 -0
  4. package/dist/cjs/adapters/fetch-transport.js +34 -0
  5. package/dist/cjs/adapters/in-memory/fake-vivoa.d.ts +93 -0
  6. package/dist/cjs/adapters/in-memory/fake-vivoa.js +468 -0
  7. package/dist/cjs/adapters/manual-clock.d.ts +9 -0
  8. package/dist/cjs/adapters/manual-clock.js +20 -0
  9. package/dist/cjs/adapters/node-crypto.d.ts +2 -0
  10. package/dist/cjs/adapters/node-crypto.js +13 -0
  11. package/dist/cjs/adapters/system-clock.d.ts +2 -0
  12. package/dist/cjs/adapters/system-clock.js +7 -0
  13. package/dist/cjs/application/partner-client.d.ts +45 -0
  14. package/dist/cjs/application/partner-client.js +55 -0
  15. package/dist/cjs/application/resources/fleet.d.ts +7 -0
  16. package/dist/cjs/application/resources/fleet.js +13 -0
  17. package/dist/cjs/application/resources/me.d.ts +13 -0
  18. package/dist/cjs/application/resources/me.js +23 -0
  19. package/dist/cjs/application/resources/merchants.d.ts +37 -0
  20. package/dist/cjs/application/resources/merchants.js +92 -0
  21. package/dist/cjs/application/resources/webhooks.d.ts +16 -0
  22. package/dist/cjs/application/resources/webhooks.js +30 -0
  23. package/dist/cjs/application/signed-http.d.ts +41 -0
  24. package/dist/cjs/application/signed-http.js +105 -0
  25. package/dist/cjs/application/use-cases/provision-store.d.ts +50 -0
  26. package/dist/cjs/application/use-cases/provision-store.js +103 -0
  27. package/dist/cjs/domain/errors.d.ts +72 -0
  28. package/dist/cjs/domain/errors.js +111 -0
  29. package/dist/cjs/domain/merchant.d.ts +74 -0
  30. package/dist/cjs/domain/merchant.js +28 -0
  31. package/dist/cjs/domain/operator.d.ts +32 -0
  32. package/dist/cjs/domain/operator.js +6 -0
  33. package/dist/cjs/domain/signature.d.ts +21 -0
  34. package/dist/cjs/domain/signature.js +23 -0
  35. package/dist/cjs/domain/webhook.d.ts +48 -0
  36. package/dist/cjs/domain/webhook.js +10 -0
  37. package/dist/cjs/index.d.ts +25 -0
  38. package/dist/cjs/index.js +61 -0
  39. package/dist/cjs/package.json +1 -0
  40. package/dist/cjs/ports/clock.port.d.ts +5 -0
  41. package/dist/cjs/ports/clock.port.js +2 -0
  42. package/dist/cjs/ports/crypto.port.d.ts +7 -0
  43. package/dist/cjs/ports/crypto.port.js +2 -0
  44. package/dist/cjs/ports/http-transport.port.d.ts +25 -0
  45. package/dist/cjs/ports/http-transport.port.js +2 -0
  46. package/dist/cjs/ports/index.d.ts +5 -0
  47. package/dist/cjs/ports/index.js +5 -0
  48. package/dist/cjs/ports/logger.port.d.ts +6 -0
  49. package/dist/cjs/ports/logger.port.js +7 -0
  50. package/dist/cjs/testing.d.ts +12 -0
  51. package/dist/cjs/testing.js +16 -0
  52. package/dist/cjs/webhooks/verify-webhook.d.ts +21 -0
  53. package/dist/cjs/webhooks/verify-webhook.js +36 -0
  54. package/dist/esm/adapters/fetch-transport.d.ts +7 -0
  55. package/dist/esm/adapters/fetch-transport.js +30 -0
  56. package/dist/esm/adapters/in-memory/fake-vivoa.d.ts +93 -0
  57. package/dist/esm/adapters/in-memory/fake-vivoa.js +464 -0
  58. package/dist/esm/adapters/manual-clock.d.ts +9 -0
  59. package/dist/esm/adapters/manual-clock.js +16 -0
  60. package/dist/esm/adapters/node-crypto.d.ts +2 -0
  61. package/dist/esm/adapters/node-crypto.js +10 -0
  62. package/dist/esm/adapters/system-clock.d.ts +2 -0
  63. package/dist/esm/adapters/system-clock.js +4 -0
  64. package/dist/esm/application/partner-client.d.ts +45 -0
  65. package/dist/esm/application/partner-client.js +51 -0
  66. package/dist/esm/application/resources/fleet.d.ts +7 -0
  67. package/dist/esm/application/resources/fleet.js +9 -0
  68. package/dist/esm/application/resources/me.d.ts +13 -0
  69. package/dist/esm/application/resources/me.js +19 -0
  70. package/dist/esm/application/resources/merchants.d.ts +37 -0
  71. package/dist/esm/application/resources/merchants.js +87 -0
  72. package/dist/esm/application/resources/webhooks.d.ts +16 -0
  73. package/dist/esm/application/resources/webhooks.js +26 -0
  74. package/dist/esm/application/signed-http.d.ts +41 -0
  75. package/dist/esm/application/signed-http.js +101 -0
  76. package/dist/esm/application/use-cases/provision-store.d.ts +50 -0
  77. package/dist/esm/application/use-cases/provision-store.js +99 -0
  78. package/dist/esm/cli.d.ts +2 -0
  79. package/dist/esm/cli.js +106 -0
  80. package/dist/esm/domain/errors.d.ts +72 -0
  81. package/dist/esm/domain/errors.js +92 -0
  82. package/dist/esm/domain/merchant.d.ts +74 -0
  83. package/dist/esm/domain/merchant.js +23 -0
  84. package/dist/esm/domain/operator.d.ts +32 -0
  85. package/dist/esm/domain/operator.js +3 -0
  86. package/dist/esm/domain/signature.d.ts +21 -0
  87. package/dist/esm/domain/signature.js +19 -0
  88. package/dist/esm/domain/webhook.d.ts +48 -0
  89. package/dist/esm/domain/webhook.js +7 -0
  90. package/dist/esm/index.d.ts +25 -0
  91. package/dist/esm/index.js +33 -0
  92. package/dist/esm/ports/clock.port.d.ts +5 -0
  93. package/dist/esm/ports/clock.port.js +1 -0
  94. package/dist/esm/ports/crypto.port.d.ts +7 -0
  95. package/dist/esm/ports/crypto.port.js +1 -0
  96. package/dist/esm/ports/http-transport.port.d.ts +25 -0
  97. package/dist/esm/ports/http-transport.port.js +1 -0
  98. package/dist/esm/ports/index.d.ts +5 -0
  99. package/dist/esm/ports/index.js +1 -0
  100. package/dist/esm/ports/logger.port.d.ts +6 -0
  101. package/dist/esm/ports/logger.port.js +4 -0
  102. package/dist/esm/testing.d.ts +12 -0
  103. package/dist/esm/testing.js +11 -0
  104. package/dist/esm/webhooks/verify-webhook.d.ts +21 -0
  105. package/dist/esm/webhooks/verify-webhook.js +33 -0
  106. package/package.json +56 -0
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MeResource = void 0;
4
+ class MeResource {
5
+ http;
6
+ constructor(http) {
7
+ this.http = http;
8
+ }
9
+ /** Plan, quota usage (`storesUsed/maxStores`), allowed countries, rate limit. */
10
+ get() {
11
+ return this.http.request('GET', '/partner/v1/me');
12
+ }
13
+ /**
14
+ * Rotates the apiSecret. The new secret is returned ONCE and this client
15
+ * switches to it immediately; persist it before the process exits.
16
+ */
17
+ async rotateSecret() {
18
+ const rotated = await this.http.request('POST', '/partner/v1/me/rotate-secret');
19
+ this.http.setApiSecret(rotated.apiSecret);
20
+ return rotated;
21
+ }
22
+ }
23
+ exports.MeResource = MeResource;
@@ -0,0 +1,37 @@
1
+ import type { SignedHttpClient } from '../signed-http.ts';
2
+ import { type CreateMerchantInput, type ListMerchantsQuery, type Merchant, type MerchantStatus, type Page } from '../../domain/merchant.ts';
3
+ /** Provisioning is synchronous on the platform: allow it minutes, not seconds. */
4
+ export declare const DEFAULT_ACTIVATE_TIMEOUT_MS: number;
5
+ export declare class MerchantsResource {
6
+ private readonly http;
7
+ constructor(http: SignedHttpClient);
8
+ /**
9
+ * Creates owner + store in your fleet. Not live until `activate`. The
10
+ * platform assigns the handle. Pass `externalId` and replaying the same
11
+ * value returns the merchant you already created — never a second one.
12
+ */
13
+ create(input: CreateMerchantInput): Promise<Merchant>;
14
+ list(query?: ListMerchantsQuery): Promise<Page<Merchant>>;
15
+ /** Walks every page of the fleet. */
16
+ listAll(query?: Omit<ListMerchantsQuery, 'page'>): AsyncGenerator<Merchant>;
17
+ /** The merchant created with this reference, or `null` — an exact server-side match. */
18
+ findByExternalId(externalId: string): Promise<Merchant | null>;
19
+ status(merchantId: string): Promise<MerchantStatus>;
20
+ /**
21
+ * Provisions the store and installs your default plugins. 409 if it is
22
+ * already creating/live/suspended; retry allowed after `failed`.
23
+ * Prefer `client.provisionStore()` which handles all of that.
24
+ */
25
+ activate(merchantId: string, options?: {
26
+ timeoutMs?: number;
27
+ }): Promise<MerchantStatus>;
28
+ suspend(merchantId: string): Promise<{
29
+ storeId: string;
30
+ status: 'suspended';
31
+ }>;
32
+ reactivate(merchantId: string): Promise<{
33
+ storeId: string;
34
+ status: 'active';
35
+ }>;
36
+ }
37
+ export declare function validateCreate(input: CreateMerchantInput): void;
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MerchantsResource = exports.DEFAULT_ACTIVATE_TIMEOUT_MS = void 0;
4
+ exports.validateCreate = validateCreate;
5
+ const errors_ts_1 = require("../../domain/errors.js");
6
+ const merchant_ts_1 = require("../../domain/merchant.js");
7
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
8
+ const COUNTRY_RE = /^[A-Z]{2}$/;
9
+ /** Provisioning is synchronous on the platform: allow it minutes, not seconds. */
10
+ exports.DEFAULT_ACTIVATE_TIMEOUT_MS = 10 * 60_000;
11
+ class MerchantsResource {
12
+ http;
13
+ constructor(http) {
14
+ this.http = http;
15
+ }
16
+ /**
17
+ * Creates owner + store in your fleet. Not live until `activate`. The
18
+ * platform assigns the handle. Pass `externalId` and replaying the same
19
+ * value returns the merchant you already created — never a second one.
20
+ */
21
+ async create(input) {
22
+ validateCreate(input);
23
+ return this.http.request('POST', '/partner/v1/merchants', { body: input });
24
+ }
25
+ list(query = {}) {
26
+ return this.http.request('GET', '/partner/v1/merchants', {
27
+ query: { page: query.page, limit: query.limit, search: query.search, externalId: query.externalId },
28
+ });
29
+ }
30
+ /** Walks every page of the fleet. */
31
+ async *listAll(query = {}) {
32
+ const limit = query.limit ?? 100;
33
+ for (let page = 1;; page++) {
34
+ const res = await this.list({ ...query, page, limit });
35
+ yield* res.data;
36
+ if (res.data.length === 0 || page * limit >= res.meta.total)
37
+ return;
38
+ }
39
+ }
40
+ /** The merchant created with this reference, or `null` — an exact server-side match. */
41
+ async findByExternalId(externalId) {
42
+ const page = await this.list({ limit: 1, externalId });
43
+ return page.data[0] ?? null;
44
+ }
45
+ async status(merchantId) {
46
+ return this.http.request('GET', `/partner/v1/merchants/${encodeId(merchantId)}/status`);
47
+ }
48
+ /**
49
+ * Provisions the store and installs your default plugins. 409 if it is
50
+ * already creating/live/suspended; retry allowed after `failed`.
51
+ * Prefer `client.provisionStore()` which handles all of that.
52
+ */
53
+ async activate(merchantId, options = {}) {
54
+ return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/activate`, {
55
+ timeoutMs: options.timeoutMs ?? exports.DEFAULT_ACTIVATE_TIMEOUT_MS,
56
+ });
57
+ }
58
+ async suspend(merchantId) {
59
+ return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/suspend`);
60
+ }
61
+ async reactivate(merchantId) {
62
+ return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/reactivate`);
63
+ }
64
+ }
65
+ exports.MerchantsResource = MerchantsResource;
66
+ function encodeId(id) {
67
+ if (!id)
68
+ throw new errors_ts_1.VivoaValidationError('merchantId is required');
69
+ return encodeURIComponent(id);
70
+ }
71
+ function validateCreate(input) {
72
+ const problems = [];
73
+ if (!EMAIL_RE.test(input.email ?? ''))
74
+ problems.push('email must be a valid address');
75
+ if (!input.name?.trim())
76
+ problems.push('name is required');
77
+ if (!Array.isArray(input.countries) || input.countries.length === 0) {
78
+ problems.push('countries must contain at least one ISO-3166 code');
79
+ }
80
+ else if (input.countries.some((c) => !COUNTRY_RE.test(c))) {
81
+ problems.push('countries must be uppercase ISO-3166 alpha-2 codes (e.g. "SV")');
82
+ }
83
+ if (input.externalId !== undefined) {
84
+ if (!input.externalId.trim())
85
+ problems.push('externalId must not be empty');
86
+ else if (input.externalId.length > merchant_ts_1.MAX_EXTERNAL_ID_LENGTH) {
87
+ problems.push(`externalId must be at most ${merchant_ts_1.MAX_EXTERNAL_ID_LENGTH} chars`);
88
+ }
89
+ }
90
+ if (problems.length > 0)
91
+ throw new errors_ts_1.VivoaValidationError(problems.join('; '));
92
+ }
@@ -0,0 +1,16 @@
1
+ import type { SignedHttpClient } from '../signed-http.ts';
2
+ import type { SetWebhookInput, SetWebhookResult, WebhookConfig, WebhookDelivery } from '../../domain/webhook.ts';
3
+ export declare class WebhooksResource {
4
+ private readonly http;
5
+ constructor(http: SignedHttpClient);
6
+ get(): Promise<WebhookConfig>;
7
+ /** `signingSecret` comes back only when the endpoint is first created. */
8
+ set(input: SetWebhookInput): Promise<SetWebhookResult>;
9
+ remove(): Promise<WebhookConfig>;
10
+ rotateSecret(): Promise<{
11
+ signingSecret: string;
12
+ }>;
13
+ /** Sends a signed `platform.test` event to your endpoint now. */
14
+ test(): Promise<WebhookDelivery>;
15
+ deliveries(limit?: number): Promise<WebhookDelivery[]>;
16
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhooksResource = void 0;
4
+ class WebhooksResource {
5
+ http;
6
+ constructor(http) {
7
+ this.http = http;
8
+ }
9
+ get() {
10
+ return this.http.request('GET', '/partner/v1/webhooks');
11
+ }
12
+ /** `signingSecret` comes back only when the endpoint is first created. */
13
+ set(input) {
14
+ return this.http.request('PUT', '/partner/v1/webhooks', { body: input });
15
+ }
16
+ remove() {
17
+ return this.http.request('DELETE', '/partner/v1/webhooks');
18
+ }
19
+ rotateSecret() {
20
+ return this.http.request('POST', '/partner/v1/webhooks/rotate-secret');
21
+ }
22
+ /** Sends a signed `platform.test` event to your endpoint now. */
23
+ test() {
24
+ return this.http.request('POST', '/partner/v1/webhooks/test');
25
+ }
26
+ deliveries(limit = 25) {
27
+ return this.http.request('GET', '/partner/v1/webhooks/deliveries', { query: { limit } });
28
+ }
29
+ }
30
+ exports.WebhooksResource = WebhooksResource;
@@ -0,0 +1,41 @@
1
+ import type { HttpRequest, HttpTransport } from '../ports/http-transport.port.ts';
2
+ import type { CryptoProvider } from '../ports/crypto.port.ts';
3
+ import type { Clock } from '../ports/clock.port.ts';
4
+ import type { SdkLogger } from '../ports/logger.port.ts';
5
+ export type HttpMethod = HttpRequest['method'];
6
+ export type QueryValue = string | number | boolean | undefined | null;
7
+ export interface SignedHttpConfig {
8
+ /** Origin of the platform API, e.g. `https://api.vivoa.app` (no path). */
9
+ baseUrl: string;
10
+ apiKey: string;
11
+ apiSecret: string;
12
+ transport: HttpTransport;
13
+ crypto: CryptoProvider;
14
+ clock: Clock;
15
+ logger: SdkLogger;
16
+ /** Default per-request timeout. */
17
+ timeoutMs: number;
18
+ /** Retries for safe (GET) requests on 429 / 5xx / network errors. */
19
+ maxRetries: number;
20
+ userAgent: string;
21
+ }
22
+ export interface RequestOptions {
23
+ body?: unknown;
24
+ query?: Record<string, QueryValue>;
25
+ timeoutMs?: number;
26
+ }
27
+ /**
28
+ * The only place that talks to the transport. Signs every attempt with a fresh
29
+ * timestamp over the EXACT bytes it sends, parses JSON and turns non-2xx into a
30
+ * typed `VivoaApiError`. Only GETs are retried: writes are not idempotent on
31
+ * the platform yet, so a retried POST could create a second merchant.
32
+ */
33
+ export declare class SignedHttpClient {
34
+ private readonly config;
35
+ private apiSecret;
36
+ constructor(config: SignedHttpConfig);
37
+ /** Used after `me.rotateSecret()` — the old secret stops working immediately. */
38
+ setApiSecret(secret: string): void;
39
+ request<T>(method: HttpMethod, path: string, options?: RequestOptions): Promise<T>;
40
+ private sign;
41
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SignedHttpClient = void 0;
4
+ const errors_ts_1 = require("../domain/errors.js");
5
+ const signature_ts_1 = require("../domain/signature.js");
6
+ const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
7
+ const BACKOFF_BASE_MS = 500;
8
+ const BACKOFF_MAX_MS = 15_000;
9
+ /**
10
+ * The only place that talks to the transport. Signs every attempt with a fresh
11
+ * timestamp over the EXACT bytes it sends, parses JSON and turns non-2xx into a
12
+ * typed `VivoaApiError`. Only GETs are retried: writes are not idempotent on
13
+ * the platform yet, so a retried POST could create a second merchant.
14
+ */
15
+ class SignedHttpClient {
16
+ config;
17
+ apiSecret;
18
+ constructor(config) {
19
+ if (!config.apiKey)
20
+ throw new Error('apiKey is required');
21
+ if (!config.apiSecret)
22
+ throw new Error('apiSecret is required');
23
+ this.config = { ...config, baseUrl: config.baseUrl.replace(/\/+$/, '') };
24
+ this.apiSecret = config.apiSecret;
25
+ }
26
+ /** Used after `me.rotateSecret()` — the old secret stops working immediately. */
27
+ setApiSecret(secret) {
28
+ this.apiSecret = secret;
29
+ }
30
+ async request(method, path, options = {}) {
31
+ const body = options.body === undefined ? undefined : JSON.stringify(options.body);
32
+ const url = this.config.baseUrl + path + queryString(options.query);
33
+ const timeoutMs = options.timeoutMs ?? this.config.timeoutMs;
34
+ const maxAttempts = method === 'GET' ? this.config.maxRetries + 1 : 1;
35
+ for (let attempt = 1;; attempt++) {
36
+ try {
37
+ const res = await this.config.transport.send(await this.sign({ method, url, path, body, timeoutMs }));
38
+ this.config.logger.debug('vivoa.request', { method, path, status: res.status, attempt });
39
+ if (res.status >= 200 && res.status < 300)
40
+ return parseBody(res);
41
+ throw (0, errors_ts_1.apiErrorFor)({ status: res.status, method, path, body: parseBody(res) });
42
+ }
43
+ catch (err) {
44
+ if (attempt >= maxAttempts || !isRetryable(err))
45
+ throw err;
46
+ const delay = backoffMs(attempt, err);
47
+ this.config.logger.warn('vivoa.retry', { method, path, attempt, delayMs: delay, reason: err.message });
48
+ await this.config.clock.sleep(delay);
49
+ }
50
+ }
51
+ }
52
+ async sign(params) {
53
+ const { crypto, clock, apiKey, userAgent } = this.config;
54
+ const timestamp = Math.floor(clock.nowMs() / 1000).toString();
55
+ const payload = (0, signature_ts_1.canonicalRequestPayload)({
56
+ timestamp,
57
+ method: params.method,
58
+ path: params.path,
59
+ bodySha256Hex: await crypto.sha256Hex(params.body ?? ''),
60
+ });
61
+ const headers = {
62
+ accept: 'application/json',
63
+ 'user-agent': userAgent,
64
+ [signature_ts_1.PARTNER_HEADERS.key]: apiKey,
65
+ [signature_ts_1.PARTNER_HEADERS.timestamp]: timestamp,
66
+ [signature_ts_1.PARTNER_HEADERS.signature]: await crypto.hmacSha256Hex(this.apiSecret, payload),
67
+ };
68
+ if (params.body !== undefined)
69
+ headers['content-type'] = 'application/json';
70
+ return { method: params.method, url: params.url, headers, body: params.body, timeoutMs: params.timeoutMs };
71
+ }
72
+ }
73
+ exports.SignedHttpClient = SignedHttpClient;
74
+ function queryString(query) {
75
+ if (!query)
76
+ return '';
77
+ const params = new URLSearchParams();
78
+ for (const [key, value] of Object.entries(query)) {
79
+ if (value !== undefined && value !== null && value !== '')
80
+ params.set(key, String(value));
81
+ }
82
+ const qs = params.toString();
83
+ return qs ? `?${qs}` : '';
84
+ }
85
+ function parseBody(res) {
86
+ if (!res.body)
87
+ return undefined;
88
+ try {
89
+ return JSON.parse(res.body);
90
+ }
91
+ catch {
92
+ return res.body;
93
+ }
94
+ }
95
+ function isRetryable(err) {
96
+ if (err instanceof errors_ts_1.VivoaNetworkError)
97
+ return true;
98
+ return err instanceof errors_ts_1.VivoaApiError && RETRYABLE_STATUS.has(err.status);
99
+ }
100
+ function backoffMs(attempt, err) {
101
+ const exp = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** (attempt - 1));
102
+ // Rate limits are per minute: waiting less than a few seconds just burns the next attempt.
103
+ const floor = err instanceof errors_ts_1.VivoaApiError && err.status === 429 ? 2_000 : 0;
104
+ return Math.max(floor, Math.round(exp / 2 + Math.random() * (exp / 2)));
105
+ }
@@ -0,0 +1,50 @@
1
+ import type { Clock } from '../../ports/clock.port.ts';
2
+ import type { MerchantsResource } from '../resources/merchants.ts';
3
+ import { type CreateMerchantInput, type Merchant, type MerchantRuntimeStatus, type MerchantStatus } from '../../domain/merchant.ts';
4
+ export type ProvisionPhase = 'found' | 'created' | 'activating' | 'waiting' | 'settled';
5
+ export interface ProvisionProgress {
6
+ phase: ProvisionPhase;
7
+ merchantId: string;
8
+ handle: string;
9
+ runtime?: MerchantRuntimeStatus;
10
+ }
11
+ export interface ProvisionStoreOptions {
12
+ /** Call `activate` (default true). `false` only ensures the merchant exists. */
13
+ activate?: boolean;
14
+ /** Wait for a settled runtime (live/failed/…) — default true. */
15
+ waitUntilLive?: boolean;
16
+ /** Overall budget for activation + waiting. Default 15 min. */
17
+ timeoutMs?: number;
18
+ /** Status poll cadence while `creating`. Default 5 s. */
19
+ pollIntervalMs?: number;
20
+ onProgress?: (progress: ProvisionProgress) => void;
21
+ }
22
+ export interface ProvisionStoreResult {
23
+ merchant: Merchant;
24
+ status: MerchantStatus;
25
+ /** false when a merchant with this externalId already existed in your fleet. */
26
+ created: boolean;
27
+ }
28
+ /**
29
+ * "Give me a live store" — the one call most partners need.
30
+ *
31
+ * Requires `input.externalId`: it is what makes retrying safe. Re-running
32
+ * after a crash, a timeout or a double click resumes the same merchant
33
+ * instead of creating a second one — the platform is idempotent on it (see
34
+ * `PARTNERS.md` §5.2), and it never runs out (unlike a handle, which the
35
+ * platform now assigns and a partner never sees before creation). Never
36
+ * retries a failed activation on its own.
37
+ */
38
+ export declare class ProvisionStoreUseCase {
39
+ private readonly merchants;
40
+ private readonly clock;
41
+ constructor(merchants: MerchantsResource, clock: Clock);
42
+ execute(input: CreateMerchantInput, options?: ProvisionStoreOptions): Promise<ProvisionStoreResult>;
43
+ /** Polls `GET /status` until the runtime leaves `creating`. */
44
+ waitUntilSettled(merchantId: string, options?: {
45
+ timeoutMs?: number;
46
+ pollIntervalMs?: number;
47
+ }): Promise<MerchantStatus>;
48
+ private ensureMerchant;
49
+ private activate;
50
+ }
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProvisionStoreUseCase = void 0;
4
+ const merchant_ts_1 = require("../../domain/merchant.js");
5
+ const errors_ts_1 = require("../../domain/errors.js");
6
+ const DEFAULT_TIMEOUT_MS = 15 * 60_000;
7
+ const DEFAULT_POLL_MS = 5_000;
8
+ /**
9
+ * "Give me a live store" — the one call most partners need.
10
+ *
11
+ * Requires `input.externalId`: it is what makes retrying safe. Re-running
12
+ * after a crash, a timeout or a double click resumes the same merchant
13
+ * instead of creating a second one — the platform is idempotent on it (see
14
+ * `PARTNERS.md` §5.2), and it never runs out (unlike a handle, which the
15
+ * platform now assigns and a partner never sees before creation). Never
16
+ * retries a failed activation on its own.
17
+ */
18
+ class ProvisionStoreUseCase {
19
+ merchants;
20
+ clock;
21
+ constructor(merchants, clock) {
22
+ this.merchants = merchants;
23
+ this.clock = clock;
24
+ }
25
+ async execute(input, options = {}) {
26
+ if (!input.externalId?.trim()) {
27
+ throw new errors_ts_1.VivoaValidationError('provisionStore requires input.externalId — it is the key that makes retrying safe. ' +
28
+ 'Use merchants.create() directly if you do not need that guarantee.');
29
+ }
30
+ const deadline = this.clock.nowMs() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
31
+ const progress = (phase, merchant, runtime) => options.onProgress?.({ phase, merchantId: merchant.id, handle: merchant.handle, runtime });
32
+ const { merchant, created } = await this.ensureMerchant(input);
33
+ progress(created ? 'created' : 'found', merchant, merchant.runtime);
34
+ let status = await this.merchants.status(merchant.id);
35
+ if (options.activate === false)
36
+ return { merchant, status, created };
37
+ if ((0, merchant_ts_1.canActivate)(status.deployStatus)) {
38
+ progress('activating', merchant, status.deployStatus);
39
+ status = await this.activate(merchant.id, deadline);
40
+ }
41
+ if (options.waitUntilLive !== false && !(0, merchant_ts_1.isSettled)(status.deployStatus)) {
42
+ progress('waiting', merchant, status.deployStatus);
43
+ status = await this.waitUntilSettled(merchant.id, {
44
+ timeoutMs: Math.max(0, deadline - this.clock.nowMs()),
45
+ pollIntervalMs: options.pollIntervalMs,
46
+ });
47
+ }
48
+ progress('settled', merchant, status.deployStatus);
49
+ if (status.deployStatus === 'failed') {
50
+ throw new errors_ts_1.VivoaProvisioningFailedError(merchant.id, 'the platform reported runtime status "failed"');
51
+ }
52
+ return { merchant: { ...merchant, runtime: status.deployStatus, domain: status.domain }, status, created };
53
+ }
54
+ /** Polls `GET /status` until the runtime leaves `creating`. */
55
+ async waitUntilSettled(merchantId, options = {}) {
56
+ const deadline = this.clock.nowMs() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
57
+ const interval = options.pollIntervalMs ?? DEFAULT_POLL_MS;
58
+ for (;;) {
59
+ const status = await this.merchants.status(merchantId);
60
+ if ((0, merchant_ts_1.isSettled)(status.deployStatus))
61
+ return status;
62
+ if (this.clock.nowMs() + interval > deadline) {
63
+ throw new errors_ts_1.VivoaTimeoutError(`Merchant ${merchantId} still "${status.deployStatus}" after the wait budget`);
64
+ }
65
+ await this.clock.sleep(interval);
66
+ }
67
+ }
68
+ async ensureMerchant(input) {
69
+ const existing = await this.merchants.findByExternalId(input.externalId);
70
+ if (existing)
71
+ return { merchant: existing, created: false };
72
+ // The platform is idempotent on externalId: even if a concurrent call for
73
+ // the same externalId wins the race between the check above and this
74
+ // request, `create` still resolves to ITS merchant — never a 409 or a
75
+ // second store. `created` can read `true` in that narrow window; harmless,
76
+ // since the merchant returned is the same either way.
77
+ return { merchant: await this.merchants.create(input), created: true };
78
+ }
79
+ async activate(merchantId, deadline) {
80
+ try {
81
+ return await this.merchants.activate(merchantId, {
82
+ timeoutMs: Math.max(1_000, deadline - this.clock.nowMs()),
83
+ });
84
+ }
85
+ catch (err) {
86
+ // 409: someone else already activated it. Network/timeout: the request may
87
+ // still be provisioning server-side. Either way, the status endpoint knows.
88
+ if (err instanceof errors_ts_1.VivoaConflictError || err instanceof errors_ts_1.VivoaNetworkError) {
89
+ return this.merchants.status(merchantId);
90
+ }
91
+ if (err instanceof errors_ts_1.VivoaServerError) {
92
+ const status = await this.merchants.status(merchantId).catch(() => null);
93
+ if (status?.deployStatus === 'failed') {
94
+ throw new errors_ts_1.VivoaProvisioningFailedError(merchantId, err.message, { cause: err });
95
+ }
96
+ if (status && !(0, merchant_ts_1.canActivate)(status.deployStatus))
97
+ return status;
98
+ }
99
+ throw err;
100
+ }
101
+ }
102
+ }
103
+ exports.ProvisionStoreUseCase = ProvisionStoreUseCase;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Typed error hierarchy. Every failed call surfaces as a `VivoaApiError`
3
+ * subclass keyed by HTTP status, so partners branch on `instanceof` instead of
4
+ * parsing strings.
5
+ */
6
+ export declare class VivoaError extends Error {
7
+ constructor(message: string, options?: {
8
+ cause?: unknown;
9
+ });
10
+ }
11
+ /** Input rejected client-side before any request is sent. */
12
+ export declare class VivoaValidationError extends VivoaError {
13
+ }
14
+ /** The request never produced an HTTP response (DNS, reset, timeout). */
15
+ export declare class VivoaNetworkError extends VivoaError {
16
+ }
17
+ /** A long-running wait (e.g. `waitUntilSettled`) ran out of time. */
18
+ export declare class VivoaTimeoutError extends VivoaError {
19
+ }
20
+ export declare class VivoaApiError extends VivoaError {
21
+ readonly status: number;
22
+ readonly method: string;
23
+ readonly path: string;
24
+ readonly body: unknown;
25
+ constructor(params: {
26
+ status: number;
27
+ method: string;
28
+ path: string;
29
+ message: string;
30
+ body: unknown;
31
+ });
32
+ }
33
+ /** 400 — invalid payload or country outside allowedRegions. */
34
+ export declare class VivoaBadRequestError extends VivoaApiError {
35
+ }
36
+ /** 401 — bad signature, unknown key or clock skew > 300 s. */
37
+ export declare class VivoaAuthenticationError extends VivoaApiError {
38
+ }
39
+ /** 403 — operator not active or quota full. */
40
+ export declare class VivoaForbiddenError extends VivoaApiError {
41
+ }
42
+ /** 404 — merchant does not exist or is not in your fleet. */
43
+ export declare class VivoaNotFoundError extends VivoaApiError {
44
+ }
45
+ /** 409 — handle taken, or activation on a creating/live/suspended merchant. */
46
+ export declare class VivoaConflictError extends VivoaApiError {
47
+ }
48
+ /** 429 — requests-per-minute limit for the plan. */
49
+ export declare class VivoaRateLimitError extends VivoaApiError {
50
+ }
51
+ /** 5xx. */
52
+ export declare class VivoaServerError extends VivoaApiError {
53
+ }
54
+ /** Extracts NestJS's `{ message }` (string or validation array) from a body. */
55
+ export declare function messageFromBody(body: unknown, fallback: string): string;
56
+ export declare function apiErrorFor(params: {
57
+ status: number;
58
+ method: string;
59
+ path: string;
60
+ body: unknown;
61
+ }): VivoaApiError;
62
+ /** Provisioning ended in `failed`. `activate` again (or `provisionStore`) to retry. */
63
+ export declare class VivoaProvisioningFailedError extends VivoaError {
64
+ readonly merchantId: string;
65
+ readonly reason: string;
66
+ constructor(merchantId: string, reason: string, options?: {
67
+ cause?: unknown;
68
+ });
69
+ }
70
+ /** A webhook body whose `X-Vivoa-Signature` does not verify. Reject it (401/400). */
71
+ export declare class VivoaWebhookSignatureError extends VivoaError {
72
+ }