@vivoa/partner-sdk 0.2.0 → 0.4.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 +12 -0
- package/README.md +13 -13
- package/dist/cjs/adapters/in-memory/fake-vivoa.d.ts +1 -0
- package/dist/cjs/adapters/in-memory/fake-vivoa.js +54 -6
- package/dist/cjs/application/resources/merchants.d.ts +24 -2
- package/dist/cjs/application/resources/merchants.js +46 -1
- package/dist/cjs/application/use-cases/provision-store.d.ts +33 -9
- package/dist/cjs/application/use-cases/provision-store.js +72 -20
- package/dist/cjs/domain/merchant.d.ts +146 -5
- package/dist/cjs/domain/merchant.js +3 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/adapters/in-memory/fake-vivoa.d.ts +1 -0
- package/dist/esm/adapters/in-memory/fake-vivoa.js +55 -7
- package/dist/esm/application/resources/merchants.d.ts +24 -2
- package/dist/esm/application/resources/merchants.js +47 -2
- package/dist/esm/application/use-cases/provision-store.d.ts +33 -9
- package/dist/esm/application/use-cases/provision-store.js +72 -20
- package/dist/esm/domain/merchant.d.ts +146 -5
- package/dist/esm/domain/merchant.js +2 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
|
@@ -5,12 +5,12 @@ const DEFAULT_POLL_MS = 5_000;
|
|
|
5
5
|
/**
|
|
6
6
|
* "Give me a live store" — the one call most partners need.
|
|
7
7
|
*
|
|
8
|
-
* Requires `input.
|
|
9
|
-
* after a crash, a timeout or a double click resumes the same merchant
|
|
10
|
-
*
|
|
11
|
-
* `PARTNERS.md` §5.2)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* Requires `input.idempotencyKey`: it is what makes retrying safe. Re-running
|
|
9
|
+
* after a crash, a timeout or a double click resumes the same merchant instead
|
|
10
|
+
* of creating a second one — the platform is idempotent on it (see
|
|
11
|
+
* `PARTNERS.md` §5.2). `externalId` is a separate, non-unique client reference
|
|
12
|
+
* (one client may own several stores) and is NOT a dedup key. Never retries a
|
|
13
|
+
* failed activation on its own.
|
|
14
14
|
*/
|
|
15
15
|
export class ProvisionStoreUseCase {
|
|
16
16
|
merchants;
|
|
@@ -20,8 +20,8 @@ export class ProvisionStoreUseCase {
|
|
|
20
20
|
this.clock = clock;
|
|
21
21
|
}
|
|
22
22
|
async execute(input, options = {}) {
|
|
23
|
-
if (!input.
|
|
24
|
-
throw new VivoaValidationError('provisionStore requires input.
|
|
23
|
+
if (!input.idempotencyKey?.trim()) {
|
|
24
|
+
throw new VivoaValidationError('provisionStore requires input.idempotencyKey — it is the key that makes retrying safe. ' +
|
|
25
25
|
'Use merchants.create() directly if you do not need that guarantee.');
|
|
26
26
|
}
|
|
27
27
|
const deadline = this.clock.nowMs() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
@@ -42,11 +42,62 @@ export class ProvisionStoreUseCase {
|
|
|
42
42
|
pollIntervalMs: options.pollIntervalMs,
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
-
progress('settled', merchant, status.deployStatus);
|
|
46
45
|
if (status.deployStatus === 'failed' || status.deployStatus === 'unknown') {
|
|
47
|
-
|
|
46
|
+
progress('settled', merchant, status.deployStatus);
|
|
47
|
+
throw new VivoaProvisioningFailedError(merchant.id, status.lastError ?? `the platform reported runtime status "${status.deployStatus}"`);
|
|
48
|
+
}
|
|
49
|
+
// Seed plugins → locations → catalog once the store is live. Best-effort
|
|
50
|
+
// by design: a failed seed leaves the store live and reports the error, so
|
|
51
|
+
// the caller can re-run (all three are idempotent) rather than losing a
|
|
52
|
+
// provisioned store to a transient credential/catalog hiccup.
|
|
53
|
+
// Locations must come before products — products may reference locationCode.
|
|
54
|
+
let plugins;
|
|
55
|
+
let locations;
|
|
56
|
+
let products;
|
|
57
|
+
if (options.plugins?.length || options.locations?.length || options.products?.length) {
|
|
58
|
+
progress('seeding', merchant, status.deployStatus);
|
|
59
|
+
if (options.plugins?.length) {
|
|
60
|
+
plugins = await this.merchants
|
|
61
|
+
.configurePlugins(merchant.id, options.plugins)
|
|
62
|
+
.then((r) => r.plugins)
|
|
63
|
+
.catch((err) => options.plugins.map((p) => ({
|
|
64
|
+
code: p.code,
|
|
65
|
+
installed: false,
|
|
66
|
+
configured: false,
|
|
67
|
+
error: err instanceof Error ? err.message : String(err),
|
|
68
|
+
})));
|
|
69
|
+
}
|
|
70
|
+
if (options.locations?.length) {
|
|
71
|
+
locations = await this.merchants
|
|
72
|
+
.importLocations(merchant.id, options.locations)
|
|
73
|
+
.catch((err) => ({
|
|
74
|
+
total: options.locations.length,
|
|
75
|
+
created: 0,
|
|
76
|
+
updated: 0,
|
|
77
|
+
errors: [{ row: 0, code: 'seed_error', message: err instanceof Error ? err.message : String(err) }],
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
if (options.products?.length) {
|
|
81
|
+
products = await this.merchants
|
|
82
|
+
.importProducts(merchant.id, options.products)
|
|
83
|
+
.catch((err) => ({
|
|
84
|
+
total: options.products.length,
|
|
85
|
+
created: 0,
|
|
86
|
+
updated: 0,
|
|
87
|
+
skipped: 0,
|
|
88
|
+
errors: [{ row: 0, code: 'seed_error', message: err instanceof Error ? err.message : String(err) }],
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
48
91
|
}
|
|
49
|
-
|
|
92
|
+
progress('settled', merchant, status.deployStatus);
|
|
93
|
+
return {
|
|
94
|
+
merchant: { ...merchant, runtime: status.deployStatus, domain: status.domain },
|
|
95
|
+
status,
|
|
96
|
+
created,
|
|
97
|
+
...(plugins ? { plugins } : {}),
|
|
98
|
+
...(locations ? { locations } : {}),
|
|
99
|
+
...(products ? { products } : {}),
|
|
100
|
+
};
|
|
50
101
|
}
|
|
51
102
|
/** Polls `GET /status` until the runtime leaves `creating`. */
|
|
52
103
|
async waitUntilSettled(merchantId, options = {}) {
|
|
@@ -63,15 +114,16 @@ export class ProvisionStoreUseCase {
|
|
|
63
114
|
}
|
|
64
115
|
}
|
|
65
116
|
async ensureMerchant(input) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
117
|
+
// The platform is idempotent on idempotencyKey: replaying create() with the
|
|
118
|
+
// same key returns the merchant it already made — never a duplicate, never a
|
|
119
|
+
// 409. So we just create; no pre-check needed. (externalId is NOT the key: it
|
|
120
|
+
// is a non-unique client reference and may map to several of your stores.)
|
|
121
|
+
const merchant = await this.merchants.create(input);
|
|
122
|
+
// A freshly created store is always 'not_activated' with nothing provisioned
|
|
123
|
+
// yet; any further-along runtime means this call replayed an existing
|
|
124
|
+
// merchant. A replay of a never-activated merchant reads created=true —
|
|
125
|
+
// harmless, since provisionStore then activates it, which is idempotent too.
|
|
126
|
+
return { merchant, created: merchant.runtime === 'not_activated' };
|
|
75
127
|
}
|
|
76
128
|
async activate(merchantId, deadline) {
|
|
77
129
|
try {
|
|
@@ -32,6 +32,14 @@ export interface MerchantStatus {
|
|
|
32
32
|
deployStatus: MerchantRuntimeStatus;
|
|
33
33
|
domain: string | null;
|
|
34
34
|
apiBaseUrl: string | null;
|
|
35
|
+
/** ISO-8601 when the store reached `live` (null before it provisions). */
|
|
36
|
+
provisionedAt: string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Why the LAST provisioning attempt failed — surfaced only while
|
|
39
|
+
* `deployStatus === 'failed'`, null otherwise (a stale error never leaks once
|
|
40
|
+
* the store is live). The queryable "why" behind a failed activation.
|
|
41
|
+
*/
|
|
42
|
+
lastError: string | null;
|
|
35
43
|
}
|
|
36
44
|
export interface CreateMerchantInput {
|
|
37
45
|
/** Email of the store owner (receives their own merchant account). */
|
|
@@ -41,13 +49,20 @@ export interface CreateMerchantInput {
|
|
|
41
49
|
countries: string[];
|
|
42
50
|
industry?: string;
|
|
43
51
|
/**
|
|
44
|
-
* Your
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* operator, max 120 chars.
|
|
52
|
+
* Your reference for the CLIENT who owns this store (your customer id). **Not
|
|
53
|
+
* unique**: one client can own several stores, all sharing this value — list
|
|
54
|
+
* them with `merchants.list({ externalId })`. This is NOT the idempotency key
|
|
55
|
+
* (see `idempotencyKey`). Max 120 chars.
|
|
49
56
|
*/
|
|
50
57
|
externalId?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Idempotency key for THIS creation (e.g. your order id). Replaying the same
|
|
60
|
+
* key returns the SAME merchant instead of creating a duplicate — safe
|
|
61
|
+
* retries. **Unique per operator**, max 120 chars. `provisionStore()`
|
|
62
|
+
* requires it for exactly that reason. Omit it and every call creates a new
|
|
63
|
+
* store.
|
|
64
|
+
*/
|
|
65
|
+
idempotencyKey?: string;
|
|
51
66
|
}
|
|
52
67
|
/** Options for `merchants.adminTicket()`. */
|
|
53
68
|
export interface AdminTicketOptions {
|
|
@@ -65,6 +80,130 @@ export interface AdminTicketOptions {
|
|
|
65
80
|
export interface MerchantAdminTicket {
|
|
66
81
|
redirectUrl: string;
|
|
67
82
|
}
|
|
83
|
+
/** One plugin to install + configure on a merchant's store. */
|
|
84
|
+
export interface PluginProvisionInput {
|
|
85
|
+
/** Plugin code (e.g. `boxful`). */
|
|
86
|
+
code: string;
|
|
87
|
+
/** Global-scope config, merged into the plugin's existing config. */
|
|
88
|
+
global?: Record<string, unknown>;
|
|
89
|
+
/** Per-country config slices (credentials, pickup origin…), merged per country. */
|
|
90
|
+
countries?: Array<{
|
|
91
|
+
countryCode: string;
|
|
92
|
+
config: Record<string, unknown>;
|
|
93
|
+
}>;
|
|
94
|
+
/** Activate the plugin (and its countries) so the store is ready to use. */
|
|
95
|
+
activate?: boolean;
|
|
96
|
+
}
|
|
97
|
+
/** Outcome of `merchants.configurePlugins()` for one plugin. */
|
|
98
|
+
export interface PluginProvisionResult {
|
|
99
|
+
code: string;
|
|
100
|
+
installed: boolean;
|
|
101
|
+
configured: boolean;
|
|
102
|
+
error?: string;
|
|
103
|
+
}
|
|
104
|
+
/** Valid product type slugs (`physical` | `service` | `digital`). Unknown slugs fall back to `physical` store-side. */
|
|
105
|
+
export type ProductTypeSlug = 'physical' | 'service' | 'digital';
|
|
106
|
+
/**
|
|
107
|
+
* One inventory entry: stock by country (auto-creates a WH-XX location) or by an
|
|
108
|
+
* existing `locationCode`. `quantity` is a non-negative integer (the store
|
|
109
|
+
* rejects fractional stock).
|
|
110
|
+
*/
|
|
111
|
+
export type InventoryEntry = {
|
|
112
|
+
country: string;
|
|
113
|
+
quantity: number;
|
|
114
|
+
locationCode?: never;
|
|
115
|
+
} | {
|
|
116
|
+
locationCode: string;
|
|
117
|
+
quantity: number;
|
|
118
|
+
country?: never;
|
|
119
|
+
};
|
|
120
|
+
/** One product to seed into a merchant's catalog. Mirrors the store import item. */
|
|
121
|
+
export interface ProvisionProduct {
|
|
122
|
+
/** Slug, kebab-case `[a-z0-9-]`. */
|
|
123
|
+
handle: string;
|
|
124
|
+
name: string;
|
|
125
|
+
/**
|
|
126
|
+
* Product type: `physical` (has variants, requires shipping), `service` (no
|
|
127
|
+
* variants, no shipping), `digital` (has variants, no shipping). Unknown
|
|
128
|
+
* values fall back to `physical` store-side — prefer being explicit.
|
|
129
|
+
*/
|
|
130
|
+
productTypeSlug: ProductTypeSlug;
|
|
131
|
+
price: number;
|
|
132
|
+
description?: string;
|
|
133
|
+
vendor?: string;
|
|
134
|
+
tags?: string[];
|
|
135
|
+
costPrice?: number;
|
|
136
|
+
barcode?: string;
|
|
137
|
+
weight?: number;
|
|
138
|
+
/** Remote image URLs — validated + ingested store-side. Max 20. */
|
|
139
|
+
images?: string[];
|
|
140
|
+
/**
|
|
141
|
+
* Sales channel handles to publish this product on. Omit to publish on the
|
|
142
|
+
* store's default channel.
|
|
143
|
+
*/
|
|
144
|
+
channels?: string[];
|
|
145
|
+
/** Per-country or per-location stock. Import locations before products when using `locationCode`. */
|
|
146
|
+
inventory?: InventoryEntry[];
|
|
147
|
+
}
|
|
148
|
+
/** One row that failed during a product import. Mirrors the store import error. */
|
|
149
|
+
export interface ImportProductError {
|
|
150
|
+
/** Zero-based index into the `products` array you sent. */
|
|
151
|
+
row: number;
|
|
152
|
+
/** Handle of the offending product, when the row carried one. */
|
|
153
|
+
handle?: string;
|
|
154
|
+
/** Machine code, e.g. `IMAGE_INVALID_URL`, `UNKNOWN_LOCATION`, `UNKNOWN_PRODUCT_TYPE`. */
|
|
155
|
+
code: string;
|
|
156
|
+
message: string;
|
|
157
|
+
/** Extra diagnostic payload, when the store attaches one. */
|
|
158
|
+
context?: Record<string, unknown>;
|
|
159
|
+
}
|
|
160
|
+
/** Aggregate result of `merchants.importProducts()`. */
|
|
161
|
+
export interface ImportProductsResult {
|
|
162
|
+
/** Rows received (== `products.length`, or the rows that passed validation). */
|
|
163
|
+
total: number;
|
|
164
|
+
created: number;
|
|
165
|
+
updated: number;
|
|
166
|
+
skipped: number;
|
|
167
|
+
/** Per-row failures. Empty on full success. */
|
|
168
|
+
errors: ImportProductError[];
|
|
169
|
+
}
|
|
170
|
+
/** One warehouse / pickup point to seed into a merchant's inventory. Upserted by `code`. */
|
|
171
|
+
export interface ProvisionLocation {
|
|
172
|
+
/** Your stable identifier — the upsert key (e.g. warehouse id). Max 60 chars. */
|
|
173
|
+
code: string;
|
|
174
|
+
/** Display name shown in the store admin. Max 120 chars. */
|
|
175
|
+
name: string;
|
|
176
|
+
/** ISO-3166-1 alpha-2 (e.g. `"SV"`, `"GT"`, `"HN"`). */
|
|
177
|
+
country: string;
|
|
178
|
+
/** Location type hint (e.g. `warehouse`, `pickup`, `shop`). */
|
|
179
|
+
type?: string;
|
|
180
|
+
address?: string;
|
|
181
|
+
city?: string;
|
|
182
|
+
postalCode?: string;
|
|
183
|
+
phone?: string;
|
|
184
|
+
email?: string;
|
|
185
|
+
isActive?: boolean;
|
|
186
|
+
/** Whether this location ships orders. Default `true`. */
|
|
187
|
+
handlesFulfillment?: boolean;
|
|
188
|
+
/** Whether customers can pick up here. Default `false`. */
|
|
189
|
+
pickupAvailable?: boolean;
|
|
190
|
+
/** Preferred carrier code for shipments dispatched from this location. */
|
|
191
|
+
preferredCarrier?: string;
|
|
192
|
+
latitude?: number;
|
|
193
|
+
longitude?: number;
|
|
194
|
+
referencePoint?: string;
|
|
195
|
+
}
|
|
196
|
+
/** Aggregate result of `merchants.importLocations()`. */
|
|
197
|
+
export interface ImportLocationsResult {
|
|
198
|
+
total: number;
|
|
199
|
+
created: number;
|
|
200
|
+
updated: number;
|
|
201
|
+
errors: Array<{
|
|
202
|
+
row: number;
|
|
203
|
+
code: string;
|
|
204
|
+
message: string;
|
|
205
|
+
}>;
|
|
206
|
+
}
|
|
68
207
|
export interface ListMerchantsQuery {
|
|
69
208
|
page?: number;
|
|
70
209
|
/** Max 100. */
|
|
@@ -84,6 +223,8 @@ export interface Page<T> {
|
|
|
84
223
|
}
|
|
85
224
|
/** Max length the platform accepts for `CreateMerchantInput.externalId`. */
|
|
86
225
|
export declare const MAX_EXTERNAL_ID_LENGTH = 120;
|
|
226
|
+
/** Max length the platform accepts for `CreateMerchantInput.idempotencyKey`. */
|
|
227
|
+
export declare const MAX_IDEMPOTENCY_KEY_LENGTH = 120;
|
|
87
228
|
/** Runtime states where the platform accepts `activate` (first run or retry). */
|
|
88
229
|
export declare function canActivate(runtime: MerchantRuntimeStatus): boolean;
|
|
89
230
|
/** States that will not change without a new action from the partner. */
|
|
@@ -13,6 +13,8 @@ export const MERCHANT_RUNTIME_STATUSES = [
|
|
|
13
13
|
];
|
|
14
14
|
/** Max length the platform accepts for `CreateMerchantInput.externalId`. */
|
|
15
15
|
export const MAX_EXTERNAL_ID_LENGTH = 120;
|
|
16
|
+
/** Max length the platform accepts for `CreateMerchantInput.idempotencyKey`. */
|
|
17
|
+
export const MAX_IDEMPOTENCY_KEY_LENGTH = 120;
|
|
16
18
|
/** Runtime states where the platform accepts `activate` (first run or retry). */
|
|
17
19
|
export function canActivate(runtime) {
|
|
18
20
|
return runtime === 'not_activated' || runtime === 'failed';
|
package/dist/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.
|
|
1
|
+
export declare const SDK_VERSION = "0.4.0";
|
package/dist/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = '0.
|
|
1
|
+
export const SDK_VERSION = '0.4.0';
|