@stacksjs/commerce 0.75.41 → 0.75.43

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.
@@ -6,5 +6,11 @@ import type { Customer, ModelRow, NewModelData } from '@stacksjs/orm';
6
6
  * @returns The newly created customer record
7
7
  */
8
8
  export declare function store(data: NewCustomer): Promise<CustomerJsonResponse>;
9
+ /**
10
+ * The avatar a customer gets when nothing supplies one. `customers.avatar` is
11
+ * NOT NULL and the dashboard requires an http(s) URL there, so a row without
12
+ * one would break the customer list. The customer importer uses it too.
13
+ */
14
+ export declare const DEFAULT_CUSTOMER_AVATAR: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&fit=crop&q=60';
9
15
  declare type CustomerJsonResponse = ModelRow<typeof Customer>;
10
16
  declare type NewCustomer = NewModelData<typeof Customer>;
@@ -0,0 +1,44 @@
1
+ import type { CatalogWarning } from './types';
2
+ /**
3
+ * An email as the deduplication key: trimmed and lowercased, or null when it
4
+ * is not an address at all. The domain part is case-insensitive by spec, and
5
+ * every platform treats the local part that way in practice, so `Bob@Shop.com`
6
+ * and `bob@shop.com ` are one customer.
7
+ */
8
+ export declare function normalizeEmail(input: unknown): string | null;
9
+ /** A timestamp as ISO 8601 UTC, or null when it is missing or unreadable. */
10
+ export declare function isoTimestamp(input: unknown): string | null;
11
+ /**
12
+ * WooCommerce's `*_gmt` fields are UTC without a zone designator
13
+ * (`2026-09-14T09:12:44`), which `Date` would read as local time.
14
+ */
15
+ export declare function gmtTimestamp(input: unknown): string | null;
16
+ /** `first last`, collapsed, or null when both are empty. */
17
+ export declare function personName(...parts: unknown[]): string | null;
18
+ /** A phone number as text, capped at the `customers.phone` length. */
19
+ export declare function phoneOf(...candidates: unknown[]): string | null;
20
+ /** Address parts joined into the single line `orders.delivery_address` holds. */
21
+ export declare function addressLine(...parts: unknown[]): string | null;
22
+ /**
23
+ * A signed decimal (a string, or a JSON number) as signed integer minor units.
24
+ *
25
+ * `decimalToMinor` accepts magnitudes only, so the sign is split off first.
26
+ * A float is never multiplied: a JSON number goes through the decimal string
27
+ * it was serialized as (`numberToMinor`).
28
+ */
29
+ export declare function signedMinor(value: unknown, exponent: number): number | null;
30
+ /**
31
+ * An amount that must not be negative, as minor units, or `fallback` with a
32
+ * warning when it is missing, malformed or negative. The order tables hold
33
+ * every amount as a non-negative integer.
34
+ */
35
+ export declare function amountOrZero(value: unknown, exponent: number, label: string, externalId: string, warnings: CatalogWarning[], fallback?: number): number;
36
+ /**
37
+ * A line total spread over its quantity, in minor units, rounded half up.
38
+ * Integer arithmetic only: `(2 * total + quantity) / (2 * quantity)`, floored.
39
+ */
40
+ export declare function unitFromTotal(totalMinor: number, quantity: number): number;
41
+ /** A positive whole quantity, or null. */
42
+ export declare function quantityOf(value: unknown): number | null;
43
+ /** A three-letter ISO 4217 code, uppercased, or null. */
44
+ export declare function currencyCode(value: unknown): string | null;
@@ -0,0 +1,99 @@
1
+ import type { CatalogSourceName, CatalogWarning, FetchLike } from './types';
2
+ import type { OrderStatus } from '../orders/events';
3
+ /**
4
+ * The normalized customers and orders the credentialed import maps into
5
+ * (stacksjs/stacks#2853).
6
+ *
7
+ * The catalog import reads public storefront APIs. Customers and orders are
8
+ * only on each platform's admin API, behind credentials: a Shopify Admin API
9
+ * access token, WooCommerce REST API consumer keys, or a Shopware Admin API
10
+ * integration. Each source is an adapter that maps its own payloads into these
11
+ * records in pure functions, so the writer never sees a platform's shape and
12
+ * every mapping is testable from recorded JSON.
13
+ *
14
+ * Money is integer minor units of the record's own currency throughout.
15
+ * Nothing here has a field for a password hash or payment card data, and no
16
+ * adapter requests one: what is not mapped cannot be written.
17
+ */
18
+ /** A customer as every source maps it. */
19
+ export declare interface AccountCustomer {
20
+ source: CatalogSourceName
21
+ externalId: string | null
22
+ email: string
23
+ name: string
24
+ phone: string | null
25
+ active: boolean | null
26
+ totalSpentMinor: number | null
27
+ currency: string | null
28
+ lastOrderAt: string | null
29
+ avatarUrl: string | null
30
+ createdAt: string | null
31
+ }
32
+ /** Who placed an order, as the order itself records it. */
33
+ export declare interface AccountOrderCustomer {
34
+ externalId: string | null
35
+ email: string | null
36
+ name: string | null
37
+ phone: string | null
38
+ }
39
+ /** One purchased line of an order. */
40
+ export declare interface AccountOrderLine {
41
+ externalId: string
42
+ productExternalId: string | null
43
+ variantExternalId: string | null
44
+ name: string
45
+ sku: string | null
46
+ quantity: number
47
+ unitPriceMinor: number
48
+ }
49
+ export declare interface AccountOrder {
50
+ source: CatalogSourceName
51
+ externalId: string
52
+ number: string | null
53
+ currency: string
54
+ status: OrderStatus
55
+ sourceStatus: string
56
+ totalMinor: number
57
+ taxMinor: number
58
+ discountMinor: number
59
+ shippingMinor: number
60
+ tipMinor: number
61
+ customer: AccountOrderCustomer | null
62
+ shippingAddress: string | null
63
+ note: string | null
64
+ placedAt: string | null
65
+ lines: AccountOrderLine[]
66
+ refundedMinor?: number
67
+ }
68
+ /** A source payload mapped to a record (or skipped), with what the mapping noticed. */
69
+ export declare interface MappedAccountRecord<T> {
70
+ record: T | null
71
+ warnings: CatalogWarning[]
72
+ }
73
+ /** One page of a source's customers or orders, already normalized. */
74
+ export declare interface AccountPage<T> {
75
+ url: string
76
+ items: T[]
77
+ warnings: CatalogWarning[]
78
+ }
79
+ export declare interface AccountFetchOptions {
80
+ credentials: AccountCredentials
81
+ limit?: number
82
+ fetch?: FetchLike
83
+ currency?: string
84
+ adminUrl?: string
85
+ sleep?: (ms: number) => Promise<void>
86
+ }
87
+ /**
88
+ * A source of customers and orders. Implement this to add a platform: map its
89
+ * payloads in pure functions, and page through its admin API here.
90
+ */
91
+ export declare interface AccountAdapter {
92
+ name: CatalogSourceName
93
+ customers: (storeUrl: string, options: AccountFetchOptions) => AsyncIterable<AccountPage<AccountCustomer>>
94
+ orders: (storeUrl: string, options: AccountFetchOptions) => AsyncIterable<AccountPage<AccountOrder>>
95
+ }
96
+ /** Credentials for one source. Read from env only, never from a flag. */
97
+ export type AccountCredentials = | { source: 'shopify', accessToken: string }
98
+ | { source: 'woocommerce', consumerKey: string, consumerSecret: string }
99
+ | { source: 'shopware', clientId: string, clientSecret: string }
@@ -0,0 +1,237 @@
1
+ import type { AccountCustomer, AccountOrder, AccountOrderLine } from './account-types';
2
+ import type { CatalogWarning } from './types';
3
+ import type { ImportAction, ImportCounts } from './writer';
4
+ /** The uuid a customer gets when an import creates it: from the source id, else from the email. */
5
+ export declare function customerUuid(source: string, host: string, externalId: string | null, email: string): string;
6
+ /**
7
+ * The account uuid a row found by email should take, or null to leave it.
8
+ *
9
+ * Only a uuid this importer derived from that same email (a guest, or a
10
+ * shopper first seen on an order) is replaced: it names no source record, so
11
+ * once the source has an account for the address, the account's id is the
12
+ * better key. A row created any other way (checkout, the dashboard, another
13
+ * store) keeps its uuid, which something else may already refer to.
14
+ */
15
+ export declare function promotedCustomerUuid(source: string, host: string, existingUuid: string | null, externalId: string | null, email: string): string | null;
16
+ /** The `customers` row for an imported customer. Pure. */
17
+ export declare function customerRow(customer: AccountCustomer, uuid: string, now: string, mode: 'create' | 'update'): CustomerRow;
18
+ /** `Name (SKU X)`: what `order_items.special_instructions` keeps of a line. */
19
+ export declare function lineLabel(line: AccountOrderLine): string;
20
+ /** The `orders` row for an imported order. Pure. */
21
+ export declare function orderRow(order: AccountOrder, host: string, customerId: number | null, now: string): OrderRow;
22
+ /**
23
+ * Fetch a store's customers through `adapter` and upsert them via
24
+ * `repository`, deduplicated by email. Idempotent: a second run over an
25
+ * unchanged store updates every row and creates none.
26
+ */
27
+ export declare function importCustomers(options: ImportCustomersOptions): Promise<CustomerImportResult>;
28
+ /**
29
+ * Fetch a store's orders through `adapter` and upsert them, with their lines,
30
+ * via `repository`. Each order's customer is the Stacks customer with its
31
+ * email (created from the order when there is none); each line links to the
32
+ * product the catalog import wrote, when it exists.
33
+ */
34
+ export declare function importOrders(options: ImportOrdersOptions): Promise<OrderImportResult>;
35
+ /**
36
+ * A repository over the commerce tables, via the query builder.
37
+ *
38
+ * Inserts are read back by `uuid`, which works on every dialect. Emails are
39
+ * compared with `LOWER()` on both sides, because rows created by checkout or
40
+ * the dashboard keep whatever case the shopper typed.
41
+ */
42
+ export declare function createDatabaseAccountRepository(connection?: any): AccountRepository;
43
+ /**
44
+ * A repository that keeps rows in memory. Pass the catalog memory
45
+ * repository's `tables` to share its products and variants, as a real
46
+ * database would.
47
+ */
48
+ export declare function createMemoryAccountRepository(shared?: { tables: Record<string, Array<Record<string, any>>> }): AccountRepository & { tables: Record<string, Array<Record<string, any>>> };
49
+ /**
50
+ * Reads through to `inner`, writes nothing. What `--dry-run` uses, so its
51
+ * report says "create" or "update" exactly as a real run would.
52
+ */
53
+ export declare function createReadOnlyAccountRepository(inner: AccountRepository): AccountRepository;
54
+ /**
55
+ * Writing imported customers and orders into the built-in commerce tables.
56
+ *
57
+ * Where each field lands, given the columns those tables actually have:
58
+ *
59
+ * | Source | Column |
60
+ * |---------------------------------|-----------------------------------------------------|
61
+ * | customer email (trim, lowercase)| `customers.email`, the deduplication key |
62
+ * | customer id + store host | `customers.uuid` on create (UUIDv5, `identity.ts`) |
63
+ * | first + last name | `customers.name` (the email when there is none) |
64
+ * | phone | `customers.phone` |
65
+ * | lifetime spend, when reported | `customers.total_spent`, integer minor units |
66
+ * | last order date, when reported | `customers.last_order`, and moved forward (never |
67
+ * | or placed by an imported order| back) to the latest order the import links to it |
68
+ * | account disabled (Shopware) | `customers.status` `Inactive` |
69
+ * | order id + store host | `orders.uuid` (UUIDv5) |
70
+ * | mapped status | `orders.status` (the `OrderStatus` vocabulary) |
71
+ * | currency | `orders.currency`, per order |
72
+ * | total / tax / discount / shipping / tip | `orders.total_amount` / `tax_amount` / `discount_amount` / `delivery_fee` / `tip_amount`, minor units |
73
+ * | shipping address, one line | `orders.delivery_address` |
74
+ * | customer note | `orders.special_instructions` |
75
+ * | placed at | `orders.created_at` |
76
+ * | each line | `order_items`: quantity, unit `price`, `product_id` |
77
+ * | line label and SKU | `order_items.special_instructions` |
78
+ *
79
+ * Customers are deduplicated by email, case-insensitively. A customer is
80
+ * matched first by the uuid an earlier run derived from its source id (so a
81
+ * changed email updates the row instead of adding one), then by email, so a
82
+ * customer who already exists in Stacks (a checkout, another store) is updated
83
+ * rather than duplicated. A row an earlier run created for a guest (its uuid
84
+ * derived from the email) takes the account's uuid once the shopper has an
85
+ * account at the source, so a later change of address there still finds it.
86
+ * Nothing password- or card-shaped is ever written: the normalized records
87
+ * have no field for either.
88
+ *
89
+ * Orders are matched on their derived uuid. A re-run updates the order and
90
+ * replaces its lines, since `order_items` has no identity column of its own;
91
+ * the lines of an imported order belong to the source. A line is linked to the
92
+ * product the catalog import wrote, through the same deterministic identities
93
+ * (`catalogUuid`). A line whose product is not in Stacks yet is kept, with no
94
+ * `product_id`, its label and price intact, and reported: import the catalog,
95
+ * re-run the order import, and the lines link. A line that names no product
96
+ * at the source (a fee, a custom item, a product deleted since) is kept the
97
+ * same way and counted apart, since no catalog import will ever link it.
98
+ *
99
+ * Writes go through the query builder, not the models, so importing a store's
100
+ * history fires no `order:created` events or emails to its customers.
101
+ */
102
+ export declare interface CustomerRow {
103
+ uuid?: string
104
+ name: string
105
+ email?: string
106
+ phone?: string | null
107
+ total_spent?: number
108
+ last_order?: string | null
109
+ status?: 'Active' | 'Inactive'
110
+ avatar?: string
111
+ created_at?: string
112
+ updated_at: string
113
+ }
114
+ export declare interface OrderRow {
115
+ uuid: string
116
+ status: string
117
+ total_amount: number
118
+ currency: string
119
+ tax_amount: number
120
+ discount_amount: number
121
+ delivery_fee: number
122
+ tip_amount: number
123
+ order_type: string
124
+ delivery_address: string | null
125
+ special_instructions: string | null
126
+ customer_id?: number | null
127
+ created_at: string
128
+ updated_at: string
129
+ }
130
+ export declare interface OrderItemRow {
131
+ order_id: number
132
+ product_id: number | null
133
+ quantity: number
134
+ price: number
135
+ special_instructions: string
136
+ created_at: string
137
+ updated_at: string
138
+ }
139
+ export declare interface ExistingCustomer {
140
+ id: number
141
+ uuid: string | null
142
+ email: string
143
+ }
144
+ /**
145
+ * Where the importer reads and writes customers and orders. The database
146
+ * implementation is below; tests and `--dry-run` use the in-memory and
147
+ * read-only ones. Every `find*` takes a whole page's keys at once.
148
+ */
149
+ export declare interface AccountRepository {
150
+ findCustomers: (query: { uuids: string[], emails: string[] }) => Promise<ExistingCustomer[]>
151
+ createCustomer: (row: CustomerRow) => Promise<number>
152
+ updateCustomer: (id: number, row: CustomerUpdate) => Promise<void>
153
+ advanceLastOrder?: (id: number, at: string) => Promise<void>
154
+ findOrders: (uuids: string[]) => Promise<Map<string, number>>
155
+ createOrder: (row: OrderRow) => Promise<number>
156
+ updateOrder: (id: number, row: Omit<OrderRow, 'uuid'>) => Promise<void>
157
+ replaceOrderItems: (orderId: number, rows: OrderItemRow[]) => Promise<void>
158
+ findProducts: (uuids: string[]) => Promise<Map<string, number>>
159
+ findVariantProducts: (uuids: string[]) => Promise<Map<string, number>>
160
+ transaction?: <T>(work: (repository: AccountRepository) => Promise<T>) => Promise<T>
161
+ }
162
+ export declare interface ImportedCustomer {
163
+ action: ImportAction
164
+ externalId: string | null
165
+ email: string
166
+ name: string
167
+ matchedBy: 'uuid' | 'email' | null
168
+ }
169
+ export declare interface ImportedOrder {
170
+ action: ImportAction
171
+ externalId: string
172
+ number: string | null
173
+ status: string
174
+ sourceStatus: string
175
+ totalMinor: number
176
+ currency: string
177
+ lines: number
178
+ unlinkedLines: number
179
+ productlessLines: number
180
+ refundedMinor: number
181
+ customerEmail: string | null
182
+ }
183
+ export declare interface CustomerImportResult {
184
+ source: string
185
+ storeUrl: string
186
+ dryRun: boolean
187
+ pages: string[]
188
+ customers: ImportedCustomer[]
189
+ counts: {
190
+ customers: ImportCounts
191
+ merged: number
192
+ }
193
+ warnings: CatalogWarning[]
194
+ }
195
+ export declare interface OrderImportResult {
196
+ source: string
197
+ storeUrl: string
198
+ dryRun: boolean
199
+ pages: string[]
200
+ orders: ImportedOrder[]
201
+ counts: {
202
+ orders: ImportCounts
203
+ customers: ImportCounts
204
+ lines: { linked: number, unlinked: number, productless: number }
205
+ duplicates: number
206
+ partlyRefunded: number
207
+ }
208
+ currencies: string[]
209
+ warnings: CatalogWarning[]
210
+ }
211
+ declare interface AccountImportOptions extends AccountFetchOptions {
212
+ adapter: AccountAdapter
213
+ storeUrl: string
214
+ repository: AccountRepository
215
+ dryRun?: boolean
216
+ now?: () => Date
217
+ }
218
+ export declare interface ImportCustomersOptions extends AccountImportOptions {
219
+ onCustomer?: (customer: ImportedCustomer) => void
220
+ }
221
+ export declare interface ImportOrdersOptions extends AccountImportOptions {
222
+ onOrder?: (order: ImportedOrder) => void
223
+ }
224
+ /** The columns an update writes; anything left out keeps its value. */
225
+ export type CustomerUpdate = Partial<CustomerRow> & { updated_at: string }
226
+ /**
227
+ * Resolves customers to row ids by uuid, then by email, across a whole run.
228
+ * Lookups are batched per page; rows created during the run are remembered so
229
+ * the next order for the same email finds them without a query.
230
+ */
231
+ declare class CustomerIndex {
232
+ load(repository: AccountRepository, keys: Array<{ uuid: string | null, email: string | null }>): Promise<void>;
233
+ remember(id: number, uuid: string | null, email: string | null): void;
234
+ find(uuid: string | null, email: string | null): { id: number, matchedBy: 'uuid' | 'email' } | null;
235
+ uuidOf(id: number): string | null;
236
+ emailTakenByOther(email: string, id: number): boolean;
237
+ }
@@ -0,0 +1,30 @@
1
+ import { CatalogFetchError } from './http';
2
+ import type { FetchLike } from './types';
3
+ import type { JsonRequest, JsonResponse } from './http';
4
+ /** One admin API call, with rate-limit retries and credential-aware errors. */
5
+ export declare function adminRequest<T>(url: string, options: AdminRequestOptions): Promise<JsonResponse<T>>;
6
+ /**
7
+ * Authenticated requests to a platform's admin API.
8
+ *
9
+ * On top of `fetchJson` (which names the failing URL), this:
10
+ *
11
+ * - waits out a 429 for as long as `Retry-After` asks (Shopify sends it on
12
+ * every throttled call; its REST leaky bucket refills at 2 calls a second),
13
+ * then retries, up to `MAX_RETRIES` times;
14
+ * - turns a 401 or 403 into a message about the credential, saying which
15
+ * variables to check and where to create them, without ever repeating a
16
+ * secret: every message is scrubbed of the credential values, including
17
+ * any a server echoed back in its error body.
18
+ */
19
+ export declare const MAX_RETRIES: 5;
20
+ export declare const defaultSleep: (ms: number) => Promise<void>;
21
+ export declare interface AdminRequestOptions extends JsonRequest {
22
+ fetch: FetchLike
23
+ secrets: string[]
24
+ credentialHelp: string
25
+ hint?: string
26
+ sleep?: (ms: number) => Promise<void>
27
+ }
28
+ export declare class AdminCredentialError extends CatalogFetchError {
29
+ constructor(url: string, message: string, status: number);
30
+ }
@@ -0,0 +1,44 @@
1
+ import type { AccountCredentials } from './account-types';
2
+ /**
3
+ * The credentials `source` needs, from `env`, or a `MissingCredentialsError`
4
+ * naming every variable that is unset and where to create them. The values
5
+ * themselves never appear in a message.
6
+ */
7
+ export declare function readAccountCredentials(source: string, env: Env): AccountCredentials;
8
+ /** Every secret value in `credentials`, for scrubbing from messages. */
9
+ export declare function credentialSecrets(credentials: AccountCredentials): string[];
10
+ /** `text` with every secret replaced, so an error never repeats a credential, even one a server echoed. */
11
+ export declare function redactSecrets(text: string, secrets: string[]): string;
12
+ /**
13
+ * Admin API credentials for the customer and order import, from env only.
14
+ *
15
+ * A credential passed as a command-line flag lands in shell history, in `ps`
16
+ * output for every user on the machine, and in CI logs that echo commands.
17
+ * There is deliberately no flag for any of these: the operator exports them,
18
+ * or puts them in `.env`, which `buddy` loads.
19
+ *
20
+ * Every check here runs before the first request, so a missing credential
21
+ * fails with the variable names and where to create them, never with a 401
22
+ * halfway through an import.
23
+ */
24
+ export declare const ACCOUNT_CREDENTIAL_ENV: {
25
+ shopify: readonly ['SHOPIFY_ADMIN_TOKEN'];
26
+ woocommerce: readonly ['WOOCOMMERCE_CONSUMER_KEY', 'WOOCOMMERCE_CONSUMER_SECRET'];
27
+ shopware: readonly ['SHOPWARE_CLIENT_ID', 'SHOPWARE_CLIENT_SECRET']
28
+ };
29
+ /**
30
+ * Where each platform issues the credentials, for every message that asks for them.
31
+ * @defaultValue
32
+ * ```ts
33
+ * {
34
+ * shopify: 'Create a custom app for the store (Shopify admin > Settings > Apps > Develop apps, or the Shopify Dev Dashboard), give it the Admin API scopes read_customers and read_orders (plus read_all_orders for orders older than 60 days), install it, and copy its Admin API access token (shpat_...).',
35
+ * woocommerce: 'Create a key in WordPress admin > WooCommerce > Settings > Advanced > REST API > Add key, with Read permission, for a user who can manage WooCommerce. The key starts with ck_ and the secret with cs_.',
36
+ * shopware: 'Create an integration in the Shopware Administration > Settings > System > Integrations, with a role that can read customers and orders. Its access key id (SWIA...) is the client id and its secret access key is the client secret.'
37
+ * }
38
+ * ```
39
+ */
40
+ export declare const ACCOUNT_CREDENTIAL_HELP: Record<keyof typeof ACCOUNT_CREDENTIAL_ENV, string>;
41
+ declare type Env = Record<string, string | undefined>;
42
+ export declare class MissingCredentialsError extends Error {
43
+ constructor(message: string);
44
+ }
@@ -9,6 +9,11 @@ import type { FetchLike } from './types';
9
9
  export declare function normalizeStoreUrl(input: string): string;
10
10
  /** The lowercased host of a normalized store URL, used to scope identities. */
11
11
  export declare function storeHost(storeUrl: string): string;
12
+ /**
13
+ * `Retry-After` as seconds. Shopify sends a decimal (`"2.0"`); the HTTP spec
14
+ * also allows an HTTP date. Anything unreadable is undefined, not zero.
15
+ */
16
+ export declare function retryAfterSeconds(header: string | null | undefined, now?: () => number): number | undefined;
12
17
  /** Fetch a URL and parse it as JSON (GET unless `request` says otherwise), or throw a `CatalogFetchError` naming it. */
13
18
  export declare function fetchJson<T = unknown>(url: string, fetcher?: FetchLike, hint?: string, request?: JsonRequest): Promise<JsonResponse<T>>;
14
19
  export declare interface JsonResponse<T> {
@@ -32,5 +37,6 @@ export declare class CatalogFetchError extends Error {
32
37
  public readonly url: string;
33
38
  public readonly status?: number;
34
39
  public readonly body?: string;
35
- constructor(url: string, message: string, status?: number, body?: string);
40
+ public readonly retryAfter?: number;
41
+ constructor(url: string, message: string, status?: number, body?: string, retryAfter?: number);
36
42
  }
@@ -19,4 +19,9 @@ export declare function catalogUuid(source: string, host: string, kind: CatalogR
19
19
  */
20
20
  /** Namespace for every UUID the catalog importer derives. Never change it. */
21
21
  export declare const CATALOG_IMPORT_NAMESPACE: '5d0c8a3e-6f1b-4c2a-9e47-b18f3d2a6c90';
22
- export type CatalogRecordKind = 'product' | 'variant';
22
+ /**
23
+ * What an identity names. `customer` and `order` come from the credentialed
24
+ * import (`accounts.ts`), and share the namespace so an order's line items
25
+ * resolve to the very `product` and `variant` uuids the catalog import wrote.
26
+ */
27
+ export type CatalogRecordKind = 'product' | 'variant' | 'customer' | 'order';
@@ -1,4 +1,16 @@
1
+ import type { AccountAdapter } from './account-types';
1
2
  import type { CatalogAdapter } from './types';
3
+ export type {
4
+ AccountAdapter,
5
+ AccountCredentials,
6
+ AccountCustomer,
7
+ AccountFetchOptions,
8
+ AccountOrder,
9
+ AccountOrderCustomer,
10
+ AccountOrderLine,
11
+ AccountPage,
12
+ MappedAccountRecord,
13
+ } from './account-types';
2
14
  export type {
3
15
  CatalogAdapter,
4
16
  CatalogCategory,
@@ -14,6 +26,8 @@ export type {
14
26
  } from './types';
15
27
  /** The adapter registered under `name`, case-insensitively. */
16
28
  export declare function catalogAdapter(name: string): CatalogAdapter | undefined;
29
+ /** The customer and order adapter registered under `name`, case-insensitively. */
30
+ export declare function accountAdapter(name: string): AccountAdapter | undefined;
17
31
  /**
18
32
  * Catalog import from other commerce platforms (stacksjs/stacks#1319, #1320,
19
33
  * #1321).
@@ -25,13 +39,27 @@ export declare function catalogAdapter(name: string): CatalogAdapter | undefined
25
39
  *
26
40
  * Adding a platform means writing a `CatalogAdapter` (a pure payload mapper
27
41
  * plus a pager) and registering it in `catalogAdapters`.
42
+ *
43
+ * Customers and orders (stacksjs/stacks#2853) come from each platform's admin
44
+ * API instead, with credentials read from env only (`credentials.ts`):
45
+ * `buddy commerce:import <store-url> --from <platform> --customers --orders`.
46
+ * Those adapters are `AccountAdapter`s, registered in `accountAdapters`, and
47
+ * written by `importCustomers` / `importOrders` (`account-writer.ts`).
28
48
  */
29
49
  export declare const catalogAdapters: Readonly<Record<string, CatalogAdapter>>;
50
+ export declare const accountAdapters: Readonly<Record<string, AccountAdapter>>;
51
+ export * from './account-mapping';
52
+ export * from './account-writer';
53
+ export * from './admin-http';
54
+ export * from './credentials';
30
55
  export * from './http';
31
56
  export * from './identity';
32
57
  export * from './money';
33
58
  export * from './shopify';
59
+ export * from './shopify-admin';
34
60
  export * from './shopware';
61
+ export * from './shopware-admin';
35
62
  export * from './text';
36
63
  export * from './woocommerce';
64
+ export * from './woocommerce-admin';
37
65
  export * from './writer';
@@ -0,0 +1,95 @@
1
+ import type { AccountAdapter, AccountCustomer, AccountOrder, MappedAccountRecord } from './account-types';
2
+ import type { OrderStatus } from '../orders/events';
3
+ /** One Admin API customer as a normalized customer, or null with the reason. Pure. */
4
+ export declare function mapShopifyCustomer(raw: ShopifyCustomerPayload): MappedAccountRecord<AccountCustomer>;
5
+ /**
6
+ * The Stacks status for a Shopify order, and whether Shopify's combination
7
+ * was one this knows. Shopify has no "delivered": a fulfilled order shipped.
8
+ */
9
+ export declare function shopifyOrderStatus(raw: Pick<ShopifyOrderPayload, 'financial_status' | 'fulfillment_status' | 'cancelled_at'>): { status: OrderStatus, mapped: boolean };
10
+ /** One Admin API order as a normalized order, or null with the reason. Pure. */
11
+ export declare function mapShopifyOrder(raw: ShopifyOrderPayload, context?: { currency?: string }): MappedAccountRecord<AccountOrder>;
12
+ /** The `page_info` cursor of the `rel="next"` entry in a `Link` header, or null on the last page. */
13
+ export declare function nextPageInfo(link: string | null | undefined): string | null;
14
+ /** The admin API base: `adminUrl` when given (a custom domain's `.myshopify.com`), else the store URL. HTTPS only. */
15
+ export declare function shopifyAdminBase(storeUrl: string, adminUrl?: string): string;
16
+ /**
17
+ * Shopify customers and orders, through the Admin REST API.
18
+ *
19
+ * Authenticated with a custom app's Admin API access token in the
20
+ * `X-Shopify-Access-Token` header (from `SHOPIFY_ADMIN_TOKEN`). Pages with
21
+ * cursor pagination: each response's `Link` header carries the next page's
22
+ * `page_info`, and a request with `page_info` may only add `limit` and
23
+ * `fields`, so every follow-up URL is rebuilt from those three.
24
+ *
25
+ * `fields` keeps each response to what the mappers read. Nothing requested
26
+ * carries a password (the Admin API never exposes one) or payment card data
27
+ * (`payment_details` and transactions are not requested).
28
+ *
29
+ * Shopify marked the REST Admin API legacy in October 2024 but keeps serving
30
+ * it to custom apps; the version is pinned so a Shopify-side change is a
31
+ * deliberate bump here, not a silent shape change.
32
+ */
33
+ export declare const SHOPIFY_ADMIN_API_VERSION: '2026-10';
34
+ export declare const shopifyAccountAdapter: AccountAdapter;
35
+ export declare interface ShopifyAddressPayload {
36
+ first_name?: string | null
37
+ last_name?: string | null
38
+ name?: string | null
39
+ address1?: string | null
40
+ address2?: string | null
41
+ city?: string | null
42
+ province?: string | null
43
+ province_code?: string | null
44
+ zip?: string | null
45
+ country?: string | null
46
+ country_code?: string | null
47
+ phone?: string | null
48
+ }
49
+ export declare interface ShopifyCustomerPayload {
50
+ id: number | string
51
+ email?: string | null
52
+ first_name?: string | null
53
+ last_name?: string | null
54
+ phone?: string | null
55
+ state?: string | null
56
+ total_spent?: string | null
57
+ currency?: string | null
58
+ created_at?: string | null
59
+ default_address?: ShopifyAddressPayload | null
60
+ }
61
+ export declare interface ShopifyLineItemPayload {
62
+ id: number | string
63
+ product_id?: number | string | null
64
+ variant_id?: number | string | null
65
+ sku?: string | null
66
+ title?: string | null
67
+ variant_title?: string | null
68
+ name?: string | null
69
+ quantity?: number | null
70
+ price?: string | null
71
+ }
72
+ export declare interface ShopifyOrderPayload {
73
+ id: number | string
74
+ name?: string | null
75
+ order_number?: number | null
76
+ email?: string | null
77
+ contact_email?: string | null
78
+ created_at?: string | null
79
+ processed_at?: string | null
80
+ cancelled_at?: string | null
81
+ currency?: string | null
82
+ total_price?: string | null
83
+ total_tax?: string | null
84
+ total_discounts?: string | null
85
+ total_shipping_price_set?: { shop_money?: { amount?: string | null, currency_code?: string | null } | null } | null
86
+ shipping_lines?: Array<{ price?: string | null }> | null
87
+ total_tip_received?: string | null
88
+ financial_status?: string | null
89
+ fulfillment_status?: string | null
90
+ customer?: (Partial<ShopifyCustomerPayload> & { id?: number | string | null }) | null
91
+ billing_address?: ShopifyAddressPayload | null
92
+ shipping_address?: ShopifyAddressPayload | null
93
+ line_items?: ShopifyLineItemPayload[] | null
94
+ note?: string | null
95
+ }