@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
|
@@ -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. */
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Pure: no I/O, no Node APIs.
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.MAX_EXTERNAL_ID_LENGTH = exports.MERCHANT_RUNTIME_STATUSES = void 0;
|
|
8
|
+
exports.MAX_IDEMPOTENCY_KEY_LENGTH = exports.MAX_EXTERNAL_ID_LENGTH = exports.MERCHANT_RUNTIME_STATUSES = void 0;
|
|
9
9
|
exports.canActivate = canActivate;
|
|
10
10
|
exports.isSettled = isSettled;
|
|
11
11
|
exports.MERCHANT_RUNTIME_STATUSES = [
|
|
@@ -18,6 +18,8 @@ exports.MERCHANT_RUNTIME_STATUSES = [
|
|
|
18
18
|
];
|
|
19
19
|
/** Max length the platform accepts for `CreateMerchantInput.externalId`. */
|
|
20
20
|
exports.MAX_EXTERNAL_ID_LENGTH = 120;
|
|
21
|
+
/** Max length the platform accepts for `CreateMerchantInput.idempotencyKey`. */
|
|
22
|
+
exports.MAX_IDEMPOTENCY_KEY_LENGTH = 120;
|
|
21
23
|
/** Runtime states where the platform accepts `activate` (first run or retry). */
|
|
22
24
|
function canActivate(runtime) {
|
|
23
25
|
return runtime === 'not_activated' || runtime === 'failed';
|
package/dist/cjs/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.
|
|
1
|
+
export declare const SDK_VERSION = "0.4.0";
|
package/dist/cjs/version.js
CHANGED
|
@@ -61,6 +61,7 @@ export declare class FakeVivoa implements HttpTransport {
|
|
|
61
61
|
private readonly operators;
|
|
62
62
|
private readonly merchants;
|
|
63
63
|
private readonly handles;
|
|
64
|
+
private readonly merchantLocations;
|
|
64
65
|
constructor(options?: FakeVivoaOptions);
|
|
65
66
|
addOperator(seed: FakeOperatorSeed): OperatorProfile;
|
|
66
67
|
setOperatorStatus(apiKey: string, status: OperatorStatus): void;
|
|
@@ -3,7 +3,7 @@ import { nodeCrypto } from "../node-crypto.js";
|
|
|
3
3
|
import { systemClock } from "../system-clock.js";
|
|
4
4
|
import { VivoaNetworkError } from "../../domain/errors.js";
|
|
5
5
|
import { canonicalRequestPayload, MAX_CLOCK_SKEW_SECONDS, PARTNER_HEADERS, } from "../../domain/signature.js";
|
|
6
|
-
import { MAX_EXTERNAL_ID_LENGTH } from "../../domain/merchant.js";
|
|
6
|
+
import { MAX_EXTERNAL_ID_LENGTH, MAX_IDEMPOTENCY_KEY_LENGTH, } from "../../domain/merchant.js";
|
|
7
7
|
import { MERCHANT_EVENT_TYPES } from "../../domain/webhook.js";
|
|
8
8
|
class HttpError extends Error {
|
|
9
9
|
status;
|
|
@@ -13,7 +13,7 @@ class HttpError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
/** `POST /merchants` body — matches CreateMerchantDto's whitelist exactly (a stale `handle` 400s, like on the real platform). */
|
|
16
|
-
const CREATE_MERCHANT_KEYS = new Set(['email', 'name', 'countries', 'industry', 'externalId']);
|
|
16
|
+
const CREATE_MERCHANT_KEYS = new Set(['email', 'name', 'countries', 'industry', 'externalId', 'idempotencyKey']);
|
|
17
17
|
/** Mirrors `shared/utils/slugify.ts` on the platform closely enough for a fake. */
|
|
18
18
|
function slugify(value) {
|
|
19
19
|
return value
|
|
@@ -51,6 +51,7 @@ export class FakeVivoa {
|
|
|
51
51
|
operators = new Map();
|
|
52
52
|
merchants = new Map();
|
|
53
53
|
handles = new Set();
|
|
54
|
+
merchantLocations = new Map();
|
|
54
55
|
constructor(options = {}) {
|
|
55
56
|
this.clock = options.clock ?? systemClock;
|
|
56
57
|
this.crypto = options.crypto ?? nodeCrypto;
|
|
@@ -152,7 +153,7 @@ export class FakeVivoa {
|
|
|
152
153
|
async route(op, method, url, rawBody) {
|
|
153
154
|
const path = url.pathname.replace(/^\/partner\/v1/, '');
|
|
154
155
|
const body = rawBody ? JSON.parse(rawBody) : {};
|
|
155
|
-
const merchantRoute = path.match(/^\/merchants\/([^/]+)\/(status|activate|suspend|reactivate|admin-ticket)$/);
|
|
156
|
+
const merchantRoute = path.match(/^\/merchants\/([^/]+)\/(status|activate|suspend|reactivate|admin-ticket|plugins|locations|products)$/);
|
|
156
157
|
if (method === 'GET' && path === '/me')
|
|
157
158
|
return this.profile(op);
|
|
158
159
|
if (method === 'POST' && path === '/me/rotate-secret') {
|
|
@@ -199,6 +200,38 @@ export class FakeVivoa {
|
|
|
199
200
|
redirectUrl: `${base}/auth/admin-redeem?token=fake-${merchant.id}${returnPath === '/admin' ? '' : `&rp=${encodeURIComponent(returnPath)}`}`,
|
|
200
201
|
};
|
|
201
202
|
}
|
|
203
|
+
if (action === 'plugins') {
|
|
204
|
+
const plugins = Array.isArray(body.plugins) ? body.plugins : [];
|
|
205
|
+
return {
|
|
206
|
+
plugins: plugins.map((p) => ({
|
|
207
|
+
code: p.code,
|
|
208
|
+
installed: true,
|
|
209
|
+
configured: true,
|
|
210
|
+
})),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (action === 'locations') {
|
|
214
|
+
const locations = Array.isArray(body.locations) ? body.locations : [];
|
|
215
|
+
if (locations.length === 0)
|
|
216
|
+
throw new HttpError(400, 'locations must not be empty');
|
|
217
|
+
const m = this.merchantLocations.get(merchant.id) ?? [];
|
|
218
|
+
let created = 0;
|
|
219
|
+
let updated = 0;
|
|
220
|
+
for (const loc of locations) {
|
|
221
|
+
if (m.some((l) => l === loc.code))
|
|
222
|
+
updated++;
|
|
223
|
+
else {
|
|
224
|
+
m.push(loc.code);
|
|
225
|
+
created++;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
this.merchantLocations.set(merchant.id, m);
|
|
229
|
+
return { total: locations.length, created, updated, errors: [] };
|
|
230
|
+
}
|
|
231
|
+
if (action === 'products') {
|
|
232
|
+
const products = Array.isArray(body.products) ? body.products : [];
|
|
233
|
+
return { total: products.length, created: products.length, updated: 0, skipped: 0, errors: [] };
|
|
234
|
+
}
|
|
202
235
|
}
|
|
203
236
|
}
|
|
204
237
|
if (method === 'GET' && path === '/fleet/analytics')
|
|
@@ -215,6 +248,7 @@ export class FakeVivoa {
|
|
|
215
248
|
const name = String(body.name ?? '');
|
|
216
249
|
const countries = Array.isArray(body.countries) ? body.countries.map(String) : [];
|
|
217
250
|
const externalId = body.externalId === undefined ? undefined : String(body.externalId);
|
|
251
|
+
const idempotencyKey = body.idempotencyKey === undefined ? undefined : String(body.idempotencyKey);
|
|
218
252
|
const problems = [];
|
|
219
253
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
|
|
220
254
|
problems.push('email must be an email');
|
|
@@ -225,11 +259,17 @@ export class FakeVivoa {
|
|
|
225
259
|
if (externalId !== undefined && externalId.length > MAX_EXTERNAL_ID_LENGTH) {
|
|
226
260
|
problems.push(`externalId must be shorter than or equal to ${MAX_EXTERNAL_ID_LENGTH} characters`);
|
|
227
261
|
}
|
|
262
|
+
if (idempotencyKey !== undefined && idempotencyKey.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
|
|
263
|
+
problems.push(`idempotencyKey must be shorter than or equal to ${MAX_IDEMPOTENCY_KEY_LENGTH} characters`);
|
|
264
|
+
}
|
|
228
265
|
if (problems.length)
|
|
229
266
|
throw new HttpError(400, problems.join('; '));
|
|
230
|
-
// Idempotent replay BEFORE quota/region checks — mirrors the platform.
|
|
231
|
-
|
|
232
|
-
|
|
267
|
+
// Idempotent replay BEFORE quota/region checks — mirrors the platform. The
|
|
268
|
+
// dedup key is idempotencyKey (unique per operator), NOT externalId: that is
|
|
269
|
+
// a non-unique client reference and may map to several of the operator's
|
|
270
|
+
// stores.
|
|
271
|
+
if (idempotencyKey) {
|
|
272
|
+
const existing = this.fleet(op).find((m) => m.idempotencyKey === idempotencyKey);
|
|
233
273
|
if (existing)
|
|
234
274
|
return publicMerchant(existing);
|
|
235
275
|
}
|
|
@@ -254,6 +294,9 @@ export class FakeVivoa {
|
|
|
254
294
|
createdAt: new Date(this.clock.nowMs()).toISOString(),
|
|
255
295
|
operatorId: op.id,
|
|
256
296
|
pollsLeft: 0,
|
|
297
|
+
idempotencyKey: idempotencyKey ?? null,
|
|
298
|
+
provisionedAt: null,
|
|
299
|
+
lastError: null,
|
|
257
300
|
};
|
|
258
301
|
this.handles.add(merchant.handle);
|
|
259
302
|
this.merchants.set(merchant.id, merchant);
|
|
@@ -288,6 +331,7 @@ export class FakeVivoa {
|
|
|
288
331
|
switch (this.activation) {
|
|
289
332
|
case 'fail':
|
|
290
333
|
m.runtime = 'failed';
|
|
334
|
+
m.lastError = this.failureReason;
|
|
291
335
|
this.emit(op, 'merchant.failed', m, { reason: this.failureReason });
|
|
292
336
|
throw new HttpError(500, this.failureReason);
|
|
293
337
|
case 'slow':
|
|
@@ -306,6 +350,8 @@ export class FakeVivoa {
|
|
|
306
350
|
goLive(m) {
|
|
307
351
|
m.runtime = 'live';
|
|
308
352
|
m.domain = `${m.handle}.vivoa.store`;
|
|
353
|
+
m.provisionedAt = new Date(this.clock.nowMs()).toISOString();
|
|
354
|
+
m.lastError = null;
|
|
309
355
|
const op = [...this.operators.values()].find((o) => o.id === m.operatorId);
|
|
310
356
|
if (op)
|
|
311
357
|
this.emit(op, 'merchant.live', m);
|
|
@@ -324,6 +370,8 @@ export class FakeVivoa {
|
|
|
324
370
|
deployStatus: m.runtime,
|
|
325
371
|
domain: m.domain,
|
|
326
372
|
apiBaseUrl: m.runtime === 'live' ? 'https://shared-api.vivoa.app' : null,
|
|
373
|
+
provisionedAt: m.provisionedAt,
|
|
374
|
+
lastError: m.runtime === 'failed' ? m.lastError : null,
|
|
327
375
|
};
|
|
328
376
|
}
|
|
329
377
|
list(op, qs) {
|
|
@@ -453,7 +501,7 @@ export class FakeVivoa {
|
|
|
453
501
|
}
|
|
454
502
|
}
|
|
455
503
|
function publicMerchant(m) {
|
|
456
|
-
const { operatorId: _op, pollsLeft: _polls, ...view } = m;
|
|
504
|
+
const { operatorId: _op, pollsLeft: _polls, idempotencyKey: _key, provisionedAt: _prov, lastError: _err, ...view } = m;
|
|
457
505
|
return { ...view, countries: [...view.countries] };
|
|
458
506
|
}
|
|
459
507
|
function webhookView(op) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SignedHttpClient } from '../signed-http.ts';
|
|
2
|
-
import { type AdminTicketOptions, type CreateMerchantInput, type ListMerchantsQuery, type Merchant, type MerchantAdminTicket, type MerchantStatus, type Page } from '../../domain/merchant.ts';
|
|
2
|
+
import { type AdminTicketOptions, type CreateMerchantInput, type ImportLocationsResult, type ImportProductsResult, type ListMerchantsQuery, type Merchant, type MerchantAdminTicket, type MerchantStatus, type Page, type PluginProvisionInput, type PluginProvisionResult, type ProvisionLocation, type ProvisionProduct } from '../../domain/merchant.ts';
|
|
3
3
|
/** Provisioning is synchronous on the platform: allow it minutes, not seconds. */
|
|
4
4
|
export declare const DEFAULT_ACTIVATE_TIMEOUT_MS: number;
|
|
5
5
|
export declare class MerchantsResource {
|
|
@@ -7,8 +7,9 @@ export declare class MerchantsResource {
|
|
|
7
7
|
constructor(http: SignedHttpClient);
|
|
8
8
|
/**
|
|
9
9
|
* Creates owner + store in your fleet. Not live until `activate`. The
|
|
10
|
-
* platform assigns the handle. Pass `
|
|
10
|
+
* platform assigns the handle. Pass `idempotencyKey` and replaying the same
|
|
11
11
|
* value returns the merchant you already created — never a second one.
|
|
12
|
+
* `externalId` is a non-unique client reference, not a dedup key.
|
|
12
13
|
*/
|
|
13
14
|
create(input: CreateMerchantInput): Promise<Merchant>;
|
|
14
15
|
list(query?: ListMerchantsQuery): Promise<Page<Merchant>>;
|
|
@@ -39,5 +40,26 @@ export declare class MerchantsResource {
|
|
|
39
40
|
* not store it. 403 if the merchant is not in your fleet or is not active.
|
|
40
41
|
*/
|
|
41
42
|
adminTicket(merchantId: string, options?: AdminTicketOptions): Promise<MerchantAdminTicket>;
|
|
43
|
+
/**
|
|
44
|
+
* Installs + configures plugins on the merchant's store (credentials, pickup
|
|
45
|
+
* origin, activation). Idempotent — config is merged, not replaced — so it
|
|
46
|
+
* doubles as a re-sync. 403 if the merchant is not in your fleet.
|
|
47
|
+
*/
|
|
48
|
+
configurePlugins(merchantId: string, plugins: PluginProvisionInput[]): Promise<{
|
|
49
|
+
plugins: PluginProvisionResult[];
|
|
50
|
+
}>;
|
|
51
|
+
/**
|
|
52
|
+
* Bulk-imports inventory locations (warehouses, pickup points) into the
|
|
53
|
+
* merchant's store. Upserted on `code` — idempotent, re-running updates
|
|
54
|
+
* rather than duplicates. Import locations **before** products when you
|
|
55
|
+
* reference `inventory[].locationCode`. 409 if the merchant is not live.
|
|
56
|
+
*/
|
|
57
|
+
importLocations(merchantId: string, locations: ProvisionLocation[]): Promise<ImportLocationsResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Bulk-imports products into the merchant's catalog (URL images, per-country
|
|
60
|
+
* inventory). Idempotent on `handle`, so re-running updates rather than
|
|
61
|
+
* duplicates. 403 if the merchant is not in your fleet.
|
|
62
|
+
*/
|
|
63
|
+
importProducts(merchantId: string, products: ProvisionProduct[]): Promise<ImportProductsResult>;
|
|
42
64
|
}
|
|
43
65
|
export declare function validateCreate(input: CreateMerchantInput): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { VivoaValidationError } from "../../domain/errors.js";
|
|
2
|
-
import { MAX_EXTERNAL_ID_LENGTH, } from "../../domain/merchant.js";
|
|
2
|
+
import { MAX_EXTERNAL_ID_LENGTH, MAX_IDEMPOTENCY_KEY_LENGTH, } from "../../domain/merchant.js";
|
|
3
3
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
4
4
|
const COUNTRY_RE = /^[A-Z]{2}$/;
|
|
5
5
|
/** Provisioning is synchronous on the platform: allow it minutes, not seconds. */
|
|
@@ -11,8 +11,9 @@ export class MerchantsResource {
|
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* Creates owner + store in your fleet. Not live until `activate`. The
|
|
14
|
-
* platform assigns the handle. Pass `
|
|
14
|
+
* platform assigns the handle. Pass `idempotencyKey` and replaying the same
|
|
15
15
|
* value returns the merchant you already created — never a second one.
|
|
16
|
+
* `externalId` is a non-unique client reference, not a dedup key.
|
|
16
17
|
*/
|
|
17
18
|
async create(input) {
|
|
18
19
|
validateCreate(input);
|
|
@@ -68,6 +69,43 @@ export class MerchantsResource {
|
|
|
68
69
|
body: options.returnPath ? { returnPath: options.returnPath } : {},
|
|
69
70
|
});
|
|
70
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Installs + configures plugins on the merchant's store (credentials, pickup
|
|
74
|
+
* origin, activation). Idempotent — config is merged, not replaced — so it
|
|
75
|
+
* doubles as a re-sync. 403 if the merchant is not in your fleet.
|
|
76
|
+
*/
|
|
77
|
+
async configurePlugins(merchantId, plugins) {
|
|
78
|
+
if (plugins.length === 0)
|
|
79
|
+
throw new VivoaValidationError('plugins must not be empty');
|
|
80
|
+
return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/plugins`, {
|
|
81
|
+
body: { plugins },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Bulk-imports inventory locations (warehouses, pickup points) into the
|
|
86
|
+
* merchant's store. Upserted on `code` — idempotent, re-running updates
|
|
87
|
+
* rather than duplicates. Import locations **before** products when you
|
|
88
|
+
* reference `inventory[].locationCode`. 409 if the merchant is not live.
|
|
89
|
+
*/
|
|
90
|
+
async importLocations(merchantId, locations) {
|
|
91
|
+
if (locations.length === 0)
|
|
92
|
+
throw new VivoaValidationError('locations must not be empty');
|
|
93
|
+
return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/locations`, {
|
|
94
|
+
body: { locations },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Bulk-imports products into the merchant's catalog (URL images, per-country
|
|
99
|
+
* inventory). Idempotent on `handle`, so re-running updates rather than
|
|
100
|
+
* duplicates. 403 if the merchant is not in your fleet.
|
|
101
|
+
*/
|
|
102
|
+
async importProducts(merchantId, products) {
|
|
103
|
+
if (products.length === 0)
|
|
104
|
+
throw new VivoaValidationError('products must not be empty');
|
|
105
|
+
return this.http.request('POST', `/partner/v1/merchants/${encodeId(merchantId)}/products`, {
|
|
106
|
+
body: { products },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
71
109
|
}
|
|
72
110
|
function encodeId(id) {
|
|
73
111
|
if (!id)
|
|
@@ -93,6 +131,13 @@ export function validateCreate(input) {
|
|
|
93
131
|
problems.push(`externalId must be at most ${MAX_EXTERNAL_ID_LENGTH} chars`);
|
|
94
132
|
}
|
|
95
133
|
}
|
|
134
|
+
if (input.idempotencyKey !== undefined) {
|
|
135
|
+
if (!input.idempotencyKey.trim())
|
|
136
|
+
problems.push('idempotencyKey must not be empty');
|
|
137
|
+
else if (input.idempotencyKey.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
|
|
138
|
+
problems.push(`idempotencyKey must be at most ${MAX_IDEMPOTENCY_KEY_LENGTH} chars`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
96
141
|
if (problems.length > 0)
|
|
97
142
|
throw new VivoaValidationError(problems.join('; '));
|
|
98
143
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Clock } from '../../ports/clock.port.ts';
|
|
2
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';
|
|
3
|
+
import { type CreateMerchantInput, type ImportLocationsResult, type ImportProductsResult, type Merchant, type MerchantRuntimeStatus, type MerchantStatus, type PluginProvisionInput, type PluginProvisionResult, type ProvisionLocation, type ProvisionProduct } from '../../domain/merchant.ts';
|
|
4
|
+
export type ProvisionPhase = 'found' | 'created' | 'activating' | 'waiting' | 'seeding' | 'settled';
|
|
5
5
|
export interface ProvisionProgress {
|
|
6
6
|
phase: ProvisionPhase;
|
|
7
7
|
merchantId: string;
|
|
@@ -18,22 +18,46 @@ export interface ProvisionStoreOptions {
|
|
|
18
18
|
/** Status poll cadence while `creating`. Default 5 s. */
|
|
19
19
|
pollIntervalMs?: number;
|
|
20
20
|
onProgress?: (progress: ProvisionProgress) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Plugins to install + configure once the store is live (credentials, pickup
|
|
23
|
+
* origin, activation). Best-effort: a live store with a failed seed still
|
|
24
|
+
* resolves — the result carries the error and the caller can re-run.
|
|
25
|
+
*/
|
|
26
|
+
plugins?: PluginProvisionInput[];
|
|
27
|
+
/**
|
|
28
|
+
* Inventory locations (warehouses, pickup points) to import once the store
|
|
29
|
+
* is live. Seeded **before** products so `inventory[].locationCode`
|
|
30
|
+
* references resolve. Best-effort.
|
|
31
|
+
*/
|
|
32
|
+
locations?: ProvisionLocation[];
|
|
33
|
+
/** Products to seed into the catalog once the store is live. Best-effort. */
|
|
34
|
+
products?: ProvisionProduct[];
|
|
21
35
|
}
|
|
22
36
|
export interface ProvisionStoreResult {
|
|
23
37
|
merchant: Merchant;
|
|
24
38
|
status: MerchantStatus;
|
|
25
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* false when this call replayed a merchant already past `not_activated` (a
|
|
41
|
+
* definite prior run). A fresh create — or a replay of a merchant that never
|
|
42
|
+
* activated — reads `true`.
|
|
43
|
+
*/
|
|
26
44
|
created: boolean;
|
|
45
|
+
/** Per-plugin seed outcome, when `options.plugins` was supplied. */
|
|
46
|
+
plugins?: PluginProvisionResult[];
|
|
47
|
+
/** Location import outcome, when `options.locations` was supplied. */
|
|
48
|
+
locations?: ImportLocationsResult;
|
|
49
|
+
/** Catalog seed outcome, when `options.products` was supplied. */
|
|
50
|
+
products?: ImportProductsResult;
|
|
27
51
|
}
|
|
28
52
|
/**
|
|
29
53
|
* "Give me a live store" — the one call most partners need.
|
|
30
54
|
*
|
|
31
|
-
* Requires `input.
|
|
32
|
-
* after a crash, a timeout or a double click resumes the same merchant
|
|
33
|
-
*
|
|
34
|
-
* `PARTNERS.md` §5.2)
|
|
35
|
-
*
|
|
36
|
-
*
|
|
55
|
+
* Requires `input.idempotencyKey`: it is what makes retrying safe. Re-running
|
|
56
|
+
* after a crash, a timeout or a double click resumes the same merchant instead
|
|
57
|
+
* of creating a second one — the platform is idempotent on it (see
|
|
58
|
+
* `PARTNERS.md` §5.2). `externalId` is a separate, non-unique client reference
|
|
59
|
+
* (one client may own several stores) and is NOT a dedup key. Never retries a
|
|
60
|
+
* failed activation on its own.
|
|
37
61
|
*/
|
|
38
62
|
export declare class ProvisionStoreUseCase {
|
|
39
63
|
private readonly merchants;
|