@saastemly/voidcommerce 0.6.0 → 0.7.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/dist/catalog.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  SIGN_IN,
15
15
  TAX,
16
16
  UI
17
- } from "./index-844b3qn9.js";
17
+ } from "./index-mnb7fz2t.js";
18
18
  import"./index-0v6na3yp.js";
19
19
  export {
20
20
  UI,
package/dist/cli.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  runVoid,
22
22
  version,
23
23
  voidAppsIn
24
- } from "./index-f1wds190.js";
24
+ } from "./index-85m0qkmg.js";
25
25
  import {
26
26
  PRIVATE_KEY_VAR,
27
27
  SECRETS_FILE,
@@ -42,7 +42,7 @@ import {
42
42
  setVariable,
43
43
  variableNames,
44
44
  verifyCloudflareToken
45
- } from "./index-2qt2yh3z.js";
45
+ } from "./index-6w8rwtsh.js";
46
46
  import {
47
47
  LAYOUTS,
48
48
  oneOrigin,
@@ -50,10 +50,10 @@ import {
50
50
  validate,
51
51
  writeManifest,
52
52
  zoneOf
53
- } from "./index-xx5p4b8d.js";
53
+ } from "./index-tjy6yygc.js";
54
54
  import {
55
55
  GROUPS
56
- } from "./index-844b3qn9.js";
56
+ } from "./index-mnb7fz2t.js";
57
57
  import {
58
58
  __require
59
59
  } from "./index-0v6na3yp.js";
@@ -1,2 +1,15 @@
1
1
  import { type Manifest } from "../manifest";
2
+ /**
3
+ * Carriers with a provider behind them, and whether each needs `lib/shipping.ts`.
4
+ *
5
+ * Exported because `generate/index.ts` has to know which shops get that file,
6
+ * and a second hand-maintained list of carrier ids WILL drift from this one —
7
+ * it already did: adding Royal Mail here left the file ungenerated, so the
8
+ * generated `auth.ts` imported a price table that did not exist.
9
+ *
10
+ * The token needs nothing: no origin, no tariff. Everything else needs one or
11
+ * the other, so the rule is simply "any carrier but the token".
12
+ */
13
+ export declare const WIRED_CARRIERS: readonly ["royal-mail", "dhl-express", "shippo", "easypost", "ups", "fedex", "nft"];
14
+ export declare const needsShippingConfig: (id: string) => boolean;
2
15
  export declare function renderAuthTs(manifest: Manifest): string;
@@ -3,7 +3,7 @@ import {
3
3
  hasFrontend,
4
4
  isApex,
5
5
  zone
6
- } from "./index-xx5p4b8d.js";
6
+ } from "./index-tjy6yygc.js";
7
7
  import {
8
8
  __require,
9
9
  __toESM
@@ -368,7 +368,7 @@ async function decryptSecrets(project) {
368
368
  }
369
369
  const expected = committedPublicKey(root);
370
370
  if (expected) {
371
- const { publicKeyFor } = await import("./keys-qwvt324e.js");
371
+ const { publicKeyFor } = await import("./keys-92tcdjrc.js");
372
372
  const derived = await publicKeyFor(privateKey);
373
373
  if (derived && derived.toLowerCase() !== expected.toLowerCase()) {
374
374
  return {
@@ -9,7 +9,7 @@ import {
9
9
  renderEnvLocal,
10
10
  renderEnvProduction,
11
11
  renderEnvTs
12
- } from "./index-2qt2yh3z.js";
12
+ } from "./index-6w8rwtsh.js";
13
13
  import {
14
14
  MANIFEST_FILE,
15
15
  has,
@@ -22,10 +22,10 @@ import {
22
22
  workerHosts,
23
23
  writeManifest,
24
24
  zone
25
- } from "./index-xx5p4b8d.js";
25
+ } from "./index-tjy6yygc.js";
26
26
  import {
27
27
  CHOICES
28
- } from "./index-844b3qn9.js";
28
+ } from "./index-mnb7fz2t.js";
29
29
  import {
30
30
  __require
31
31
  } from "./index-0v6na3yp.js";
@@ -48,10 +48,17 @@ var VAT = {
48
48
  PL: { rate: 2300, name: "VAT" }
49
49
  };
50
50
  var social = (manifest, id, envPrefix) => has(manifest, id) ? ` ${id}: { clientId: must(env, "${envPrefix}_CLIENT_ID"), clientSecret: must(env, "${envPrefix}_CLIENT_SECRET") },` : null;
51
+ var WIRED_CARRIERS = ["royal-mail", "dhl-express", "shippo", "easypost", "ups", "fedex", "nft"];
52
+ var needsShippingConfig = (id) => WIRED_CARRIERS.includes(id) && id !== "nft";
51
53
  function renderAuthTs(manifest) {
52
54
  const { shop } = manifest;
53
55
  const lines = [];
54
56
  const CARRIER_CALLS = {
57
+ "royal-mail": {
58
+ label: "Royal Mail",
59
+ call: `royalMailProvider({ apiKey: must(env, "CLICK_DROP_AUTH_KEY"), services: royalMailServices, currency: "${shop.currency}", carrierName: "Royal Mail" })`,
60
+ import: `import { royalMailProvider } from "@saastemly/better-commerce/providers/royal-mail";`
61
+ },
55
62
  "dhl-express": {
56
63
  label: "DHL Express",
57
64
  call: `dhlExpressProvider({ apiKey: must(env, "DHL_API_KEY"), apiSecret: must(env, "DHL_API_SECRET"), accountNumber: must(env, "DHL_ACCOUNT_NUMBER"), from: shipFrom })`,
@@ -86,8 +93,12 @@ import { mintProvider, destinationFor } from "./lib/mint.ts";`
86
93
  };
87
94
  const carriers = Object.keys(CARRIER_CALLS).filter((id) => has(manifest, id));
88
95
  const carrierImports = carriers.map((id) => CARRIER_CALLS[id].import);
89
- if (carriers.some((id) => id !== "nft"))
90
- carrierImports.push(`import { shipFrom } from "./lib/shipping.ts";`);
96
+ const shipping = [
97
+ ...carriers.some((id) => id !== "nft" && id !== "royal-mail") ? ["shipFrom"] : [],
98
+ ...carriers.includes("royal-mail") ? ["royalMailServices"] : []
99
+ ];
100
+ if (shipping.length > 0)
101
+ carrierImports.push(`import { ${shipping.join(", ")} } from "./lib/shipping.ts";`);
91
102
  const wantsNft = has(manifest, "nft");
92
103
  const carrierFulfillment = carriers.length ? `, deliveryFulfillment([${carriers.map((id) => CARRIER_CALLS[id].call).join(", ")}])` : "";
93
104
  const nftDefault = carriers.length ? `, defaultProvider: "delivery:${carriers.join("+")}"` : "";
@@ -1333,7 +1344,7 @@ import color from "picocolors";
1333
1344
  // package.json
1334
1345
  var package_default = {
1335
1346
  name: "@saastemly/voidcommerce",
1336
- version: "0.6.0",
1347
+ version: "0.7.0",
1337
1348
  description: "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
1338
1349
  type: "module",
1339
1350
  license: "MIT",
@@ -2181,13 +2192,18 @@ export async function destinationFor(orderId: string): Promise<string | null> {
2181
2192
  `;
2182
2193
  }
2183
2194
  function renderShippingTs(manifest) {
2184
- return `import type { DeliveryAddress } from "@saastemly/better-commerce/providers/delivery";
2185
-
2195
+ const royalMail = has(manifest, "royal-mail");
2196
+ const origin = ["dhl-express", "shippo", "easypost", "ups", "fedex"].some((id) => has(manifest, id));
2197
+ const imports = [
2198
+ ...origin ? [`import type { DeliveryAddress } from "@saastemly/better-commerce/providers/delivery";`] : [],
2199
+ ...royalMail ? [`import type { RoyalMailService } from "@saastemly/better-commerce/providers/royal-mail";`] : []
2200
+ ];
2201
+ const originBlock = `
2186
2202
  /**
2187
2203
  * The address every parcel is quoted and shipped FROM.
2188
2204
  *
2189
- * TODO VERIFY: this is a placeholder. A carrier quotes against it, and DHL
2190
- * refuses a shipment whose origin has no \`name\` and \`phone\`.
2205
+ * TODO VERIFY: this is a placeholder. A carrier quotes against it, and most
2206
+ * refuse a shipment whose origin has no \`name\` and \`phone\`.
2191
2207
  */
2192
2208
  export const shipFrom: DeliveryAddress = {
2193
2209
  name: "${manifest.shop.name}",
@@ -2198,6 +2214,9 @@ export const shipFrom: DeliveryAddress = {
2198
2214
  phone: "TODO VERIFY: +44…",
2199
2215
  };
2200
2216
  `;
2217
+ return `${imports.join(`
2218
+ `)}
2219
+ ${origin ? originBlock : ""}${royalMail ? ROYAL_MAIL_SERVICES() : ""}`;
2201
2220
  }
2202
2221
  function renderErpTs() {
2203
2222
  return `import { businessCentralProvider } from "@saastemly/better-commerce/providers/business-central";
@@ -2226,6 +2245,44 @@ export const erpFulfillment = (currency: string): FulfillmentProvider =>
2226
2245
  }).fulfillment();
2227
2246
  `;
2228
2247
  }
2248
+ var ROYAL_MAIL_SERVICES = () => `
2249
+ /**
2250
+ * What this shop charges for Royal Mail, by total parcel weight.
2251
+ *
2252
+ * TODO VERIFY: every \`amount\` below is a placeholder in pence. Replace them
2253
+ * with your own OBA rate card — Click & Drop → Settings → Shipping services
2254
+ * lists exactly which services your account has, and the codes here must be
2255
+ * among them. Service codes are account-specific: the published list is from
2256
+ * 2021 and Royal Mail warn that "available services are unique to your OBA
2257
+ * account and individual service agreements".
2258
+ *
2259
+ * The band whose \`maxGrams\` first covers the parcel wins, so order does not
2260
+ * matter. A parcel heavier than every band gets no rate at all rather than a
2261
+ * wrong one, and Click & Drop refuses anything over 30 kg.
2262
+ */
2263
+ export const royalMailServices: RoyalMailService[] = [
2264
+ {
2265
+ code: "TPN24",
2266
+ name: "Tracked 24",
2267
+ estimatedDays: 1,
2268
+ bands: [
2269
+ { maxGrams: 1000, amount: 0 /* TODO VERIFY */ },
2270
+ { maxGrams: 2000, amount: 0 /* TODO VERIFY */ },
2271
+ { maxGrams: 5000, amount: 0 /* TODO VERIFY */ },
2272
+ ],
2273
+ },
2274
+ {
2275
+ code: "TPS48",
2276
+ name: "Tracked 48",
2277
+ estimatedDays: 2,
2278
+ bands: [
2279
+ { maxGrams: 1000, amount: 0 /* TODO VERIFY */ },
2280
+ { maxGrams: 2000, amount: 0 /* TODO VERIFY */ },
2281
+ { maxGrams: 5000, amount: 0 /* TODO VERIFY */ },
2282
+ ],
2283
+ },
2284
+ ];
2285
+ `;
2229
2286
 
2230
2287
  // src/generate/index.ts
2231
2288
  async function exists(path) {
@@ -2314,7 +2371,7 @@ async function generateApi(root, dir, manifest, result, opts) {
2314
2371
  if (has(manifest, "nft")) {
2315
2372
  await put(root, at("lib/mint.ts"), renderMintTs(), result, own);
2316
2373
  }
2317
- if (["dhl-express", "shippo", "easypost", "ups", "fedex"].some((id) => has(manifest, id))) {
2374
+ if (WIRED_CARRIERS.some((id) => needsShippingConfig(id) && has(manifest, id))) {
2318
2375
  await put(root, at("lib/shipping.ts"), renderShippingTs(manifest), result, own);
2319
2376
  }
2320
2377
  if (has(manifest, "business-central")) {
@@ -446,7 +446,18 @@ var CARRIERS = {
446
446
  { id: "fedex", label: "FedEx", hint: "your own FedEx account, for negotiated rates an aggregator cannot see", env: [{ key: "FEDEX_CLIENT_ID", breaks: "FedEx rates are not quoted" }, { key: "FEDEX_CLIENT_SECRET", breaks: "the same" }, { key: "FEDEX_ACCOUNT_NUMBER", breaks: "FedEx has no account to bill the transport to" }] },
447
447
  { id: "dhl-express", label: "DHL Express", hint: "international express, and the one worth having for a shop that posts abroad", env: [{ key: "DHL_API_KEY", breaks: "DHL rates are not quoted" }, { key: "DHL_API_SECRET", breaks: "the same" }, { key: "DHL_ACCOUNT_NUMBER", breaks: "DHL has no account to bill the transport to" }] },
448
448
  { id: "usps", label: "USPS", hint: "US domestic post — the cheapest way to ship a small parcel inside the US", env: [{ key: "USPS_CLIENT_ID", breaks: "USPS rates are not quoted" }, { key: "USPS_CLIENT_SECRET", breaks: "the same" }] },
449
- { id: "royal-mail", label: "Royal Mail", hint: "UK post, for a shop whose customers are mostly in Britain", env: [{ key: "ROYAL_MAIL_CLIENT_ID", breaks: "Royal Mail rates are not quoted" }] },
449
+ {
450
+ id: "royal-mail",
451
+ label: "Royal Mail (Click & Drop)",
452
+ hint: "UK post. Needs an OBA business account — Click & Drop generates labels through the API only for those. Prices come from your own tariff table, because no Royal Mail API quotes rates",
453
+ env: [
454
+ {
455
+ key: "CLICK_DROP_AUTH_KEY",
456
+ breaks: "no label and no tracking number: orders are created in Click & Drop but never despatched",
457
+ where: "Click & Drop → Settings → Integrations → Click & Drop API"
458
+ }
459
+ ]
460
+ },
450
461
  { id: "aramex", label: "Aramex", hint: "the carrier to reach the Gulf and wider Middle East reliably", env: [{ key: "ARAMEX_API_KEY", breaks: "Aramex rates are not quoted" }] },
451
462
  { id: "doordash-drive", label: "DoorDash Drive", hint: "same-day local delivery", env: [{ key: "DOORDASH_DEVELOPER_ID", breaks: "DoorDash is not offered" }] },
452
463
  { id: "stuart", label: "Stuart", hint: "same-day courier, Europe", env: [{ key: "STUART_CLIENT_ID", breaks: "Stuart is not offered" }] },
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  CHOICES,
3
3
  GROUPS
4
- } from "./index-844b3qn9.js";
4
+ } from "./index-mnb7fz2t.js";
5
5
 
6
6
  // src/manifest.ts
7
7
  import { readFile, writeFile } from "node:fs/promises";
package/dist/index.js CHANGED
@@ -51,7 +51,7 @@ import {
51
51
  routeProblem,
52
52
  strictDependencies,
53
53
  upsertJsonc
54
- } from "./index-f1wds190.js";
54
+ } from "./index-85m0qkmg.js";
55
55
  import {
56
56
  allEnvKeys,
57
57
  envSummary,
@@ -59,7 +59,7 @@ import {
59
59
  renderEnvLocal,
60
60
  renderEnvProduction,
61
61
  renderEnvTs
62
- } from "./index-2qt2yh3z.js";
62
+ } from "./index-6w8rwtsh.js";
63
63
  import {
64
64
  LAYOUTS,
65
65
  MANIFEST_FILE,
@@ -80,7 +80,7 @@ import {
80
80
  writeManifest,
81
81
  zone,
82
82
  zoneOf
83
- } from "./index-xx5p4b8d.js";
83
+ } from "./index-tjy6yygc.js";
84
84
  import {
85
85
  AUTH_PLUGINS,
86
86
  CARRIERS,
@@ -97,7 +97,7 @@ import {
97
97
  SIGN_IN,
98
98
  TAX,
99
99
  UI
100
- } from "./index-844b3qn9.js";
100
+ } from "./index-mnb7fz2t.js";
101
101
  import"./index-0v6na3yp.js";
102
102
  export {
103
103
  zoneOf,
@@ -5,9 +5,9 @@ import {
5
5
  keysCommand,
6
6
  provisionKey,
7
7
  publicKeyFor
8
- } from "./index-2qt2yh3z.js";
9
- import"./index-xx5p4b8d.js";
10
- import"./index-844b3qn9.js";
8
+ } from "./index-6w8rwtsh.js";
9
+ import"./index-tjy6yygc.js";
10
+ import"./index-mnb7fz2t.js";
11
11
  import"./index-0v6na3yp.js";
12
12
  export {
13
13
  publicKeyFor,
package/dist/manifest.js CHANGED
@@ -18,8 +18,8 @@ import {
18
18
  writeManifest,
19
19
  zone,
20
20
  zoneOf
21
- } from "./index-xx5p4b8d.js";
22
- import"./index-844b3qn9.js";
21
+ } from "./index-tjy6yygc.js";
22
+ import"./index-mnb7fz2t.js";
23
23
  import"./index-0v6na3yp.js";
24
24
  export {
25
25
  zoneOf,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saastemly/voidcommerce",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/catalog.ts CHANGED
@@ -519,7 +519,18 @@ export const CARRIERS: Group = {
519
519
  { id: "fedex", label: "FedEx", hint: "your own FedEx account, for negotiated rates an aggregator cannot see", env: [{ key: "FEDEX_CLIENT_ID", breaks: "FedEx rates are not quoted" }, { key: "FEDEX_CLIENT_SECRET", breaks: "the same" }, { key: "FEDEX_ACCOUNT_NUMBER", breaks: "FedEx has no account to bill the transport to" }] },
520
520
  { id: "dhl-express", label: "DHL Express", hint: "international express, and the one worth having for a shop that posts abroad", env: [{ key: "DHL_API_KEY", breaks: "DHL rates are not quoted" }, { key: "DHL_API_SECRET", breaks: "the same" }, { key: "DHL_ACCOUNT_NUMBER", breaks: "DHL has no account to bill the transport to" }] },
521
521
  { id: "usps", label: "USPS", hint: "US domestic post — the cheapest way to ship a small parcel inside the US", env: [{ key: "USPS_CLIENT_ID", breaks: "USPS rates are not quoted" }, { key: "USPS_CLIENT_SECRET", breaks: "the same" }] },
522
- { id: "royal-mail", label: "Royal Mail", hint: "UK post, for a shop whose customers are mostly in Britain", env: [{ key: "ROYAL_MAIL_CLIENT_ID", breaks: "Royal Mail rates are not quoted" }] },
522
+ {
523
+ id: "royal-mail",
524
+ label: "Royal Mail (Click & Drop)",
525
+ hint: "UK post. Needs an OBA business account — Click & Drop generates labels through the API only for those. Prices come from your own tariff table, because no Royal Mail API quotes rates",
526
+ env: [
527
+ {
528
+ key: "CLICK_DROP_AUTH_KEY",
529
+ breaks: "no label and no tracking number: orders are created in Click & Drop but never despatched",
530
+ where: "Click & Drop → Settings → Integrations → Click & Drop API",
531
+ },
532
+ ],
533
+ },
523
534
  { id: "aramex", label: "Aramex", hint: "the carrier to reach the Gulf and wider Middle East reliably", env: [{ key: "ARAMEX_API_KEY", breaks: "Aramex rates are not quoted" }] },
524
535
  { id: "doordash-drive", label: "DoorDash Drive", hint: "same-day local delivery", env: [{ key: "DOORDASH_DEVELOPER_ID", breaks: "DoorDash is not offered" }] },
525
536
  { id: "stuart", label: "Stuart", hint: "same-day courier, Europe", env: [{ key: "STUART_CLIENT_ID", breaks: "Stuart is not offered" }] },
@@ -49,6 +49,20 @@ const social = (manifest: Manifest, id: string, envPrefix: string) =>
49
49
  ? ` ${id}: { clientId: must(env, "${envPrefix}_CLIENT_ID"), clientSecret: must(env, "${envPrefix}_CLIENT_SECRET") },`
50
50
  : null;
51
51
 
52
+ /**
53
+ * Carriers with a provider behind them, and whether each needs `lib/shipping.ts`.
54
+ *
55
+ * Exported because `generate/index.ts` has to know which shops get that file,
56
+ * and a second hand-maintained list of carrier ids WILL drift from this one —
57
+ * it already did: adding Royal Mail here left the file ungenerated, so the
58
+ * generated `auth.ts` imported a price table that did not exist.
59
+ *
60
+ * The token needs nothing: no origin, no tariff. Everything else needs one or
61
+ * the other, so the rule is simply "any carrier but the token".
62
+ */
63
+ export const WIRED_CARRIERS = ["royal-mail", "dhl-express", "shippo", "easypost", "ups", "fedex", "nft"] as const;
64
+ export const needsShippingConfig = (id: string): boolean => WIRED_CARRIERS.includes(id as never) && id !== "nft";
65
+
52
66
  export function renderAuthTs(manifest: Manifest): string {
53
67
  const { shop } = manifest;
54
68
  const lines: Line[] = [];
@@ -68,6 +82,14 @@ export function renderAuthTs(manifest: Manifest): string {
68
82
  const CARRIER_CALLS: Record<string, { call: string; label: string; import: string }> = {
69
83
  // Every real carrier needs the ORIGIN it ships from, which is shop data
70
84
  // rather than a credential — so it lives in an owned lib/shipping.ts.
85
+ "royal-mail": {
86
+ label: "Royal Mail",
87
+ // No `from`: Click & Drop posts from the account's registered
88
+ // address, so an origin here would be ignored. `services` is the
89
+ // price table in lib/shipping.ts, because nothing quotes Royal Mail.
90
+ call: `royalMailProvider({ apiKey: must(env, "CLICK_DROP_AUTH_KEY"), services: royalMailServices, currency: "${shop.currency}", carrierName: "Royal Mail" })`,
91
+ import: `import { royalMailProvider } from "@saastemly/better-commerce/providers/royal-mail";`,
92
+ },
71
93
  "dhl-express": {
72
94
  label: "DHL Express",
73
95
  call: `dhlExpressProvider({ apiKey: must(env, "DHL_API_KEY"), apiSecret: must(env, "DHL_API_SECRET"), accountNumber: must(env, "DHL_ACCOUNT_NUMBER"), from: shipFrom })`,
@@ -101,8 +123,18 @@ export function renderAuthTs(manifest: Manifest): string {
101
123
  };
102
124
  const carriers = Object.keys(CARRIER_CALLS).filter((id) => has(manifest, id));
103
125
  const carrierImports = carriers.map((id) => CARRIER_CALLS[id]!.import);
104
- // The token needs no origin; every other carrier does.
105
- if (carriers.some((id) => id !== "nft")) carrierImports.push(`import { shipFrom } from "./lib/shipping.ts";`);
126
+ // One import from lib/shipping.ts, naming only what is actually used —
127
+ // an unused binding is a type error in a shop with `noUnusedLocals`.
128
+ //
129
+ // The token needs no origin. Royal Mail needs no origin either: Click &
130
+ // Drop posts from the address registered on the account, so a `from` here
131
+ // would be silently ignored. It needs the price table instead, because
132
+ // nothing quotes Royal Mail.
133
+ const shipping = [
134
+ ...(carriers.some((id) => id !== "nft" && id !== "royal-mail") ? ["shipFrom"] : []),
135
+ ...(carriers.includes("royal-mail") ? ["royalMailServices"] : []),
136
+ ];
137
+ if (shipping.length > 0) carrierImports.push(`import { ${shipping.join(", ")} } from "./lib/shipping.ts";`);
106
138
  const wantsNft = has(manifest, "nft");
107
139
  // One bridge for every carrier: which ships is decided by whose rate the
108
140
  // buyer paid for, so a token order mints and a parcel order posts.
@@ -3,7 +3,7 @@ import { existsSync, lstatSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { CHOICES } from "../catalog";
5
5
  import { MANIFEST_FILE, type Manifest, has, isSingleApp, oneOrigin, packagesOf, specifier, workerHosts, writeManifest, zone } from "../manifest";
6
- import { renderAuthTs } from "./auth";
6
+ import { renderAuthTs, WIRED_CARRIERS, needsShippingConfig } from "./auth";
7
7
  import {
8
8
  envSummary,
9
9
  renderEnvExample,
@@ -189,8 +189,10 @@ async function generateApi(root: string, dir: string, manifest: Manifest, result
189
189
  if (has(manifest, "nft")) {
190
190
  await put(root, at("lib/mint.ts"), renderMintTs(), result, own);
191
191
  }
192
- // Any carrier but the token quotes against an origin, which is shop data.
193
- if (["dhl-express", "shippo", "easypost", "ups", "fedex"].some((id) => has(manifest, id))) {
192
+ // Any carrier but the token needs shop data of its own — an origin to quote
193
+ // against, or a price table. The list lives with the carriers themselves so
194
+ // it cannot drift from them.
195
+ if (WIRED_CARRIERS.some((id) => needsShippingConfig(id) && has(manifest, id))) {
194
196
  await put(root, at("lib/shipping.ts"), renderShippingTs(manifest), result, own);
195
197
  }
196
198
  if (has(manifest, "business-central")) {
@@ -274,13 +274,22 @@ export async function destinationFor(orderId: string): Promise<string | null> {
274
274
  * deploy, so both are marked rather than left blank.
275
275
  */
276
276
  export function renderShippingTs(manifest: Manifest): string {
277
- return `import type { DeliveryAddress } from "@saastemly/better-commerce/providers/delivery";
277
+ const royalMail = has(manifest, "royal-mail");
278
+ // Royal Mail posts from the address registered on the Click & Drop account,
279
+ // so an origin here would be ignored. Only carriers that QUOTE need one.
280
+ const origin = ["dhl-express", "shippo", "easypost", "ups", "fedex"].some((id) => has(manifest, id));
278
281
 
282
+ const imports = [
283
+ ...(origin ? [`import type { DeliveryAddress } from "@saastemly/better-commerce/providers/delivery";`] : []),
284
+ ...(royalMail ? [`import type { RoyalMailService } from "@saastemly/better-commerce/providers/royal-mail";`] : []),
285
+ ];
286
+
287
+ const originBlock = `
279
288
  /**
280
289
  * The address every parcel is quoted and shipped FROM.
281
290
  *
282
- * TODO VERIFY: this is a placeholder. A carrier quotes against it, and DHL
283
- * refuses a shipment whose origin has no \`name\` and \`phone\`.
291
+ * TODO VERIFY: this is a placeholder. A carrier quotes against it, and most
292
+ * refuse a shipment whose origin has no \`name\` and \`phone\`.
284
293
  */
285
294
  export const shipFrom: DeliveryAddress = {
286
295
  name: "${manifest.shop.name}",
@@ -291,6 +300,8 @@ export const shipFrom: DeliveryAddress = {
291
300
  phone: "TODO VERIFY: +44…",
292
301
  };
293
302
  `;
303
+
304
+ return `${imports.join("\n")}\n${origin ? originBlock : ""}${royalMail ? ROYAL_MAIL_SERVICES() : ""}`;
294
305
  }
295
306
 
296
307
  export function renderErpTs(): string {
@@ -320,3 +331,56 @@ export const erpFulfillment = (currency: string): FulfillmentProvider =>
320
331
  }).fulfillment();
321
332
  `;
322
333
  }
334
+
335
+
336
+ /**
337
+ * Royal Mail's price table — YOUR prices, because nothing will quote them.
338
+ *
339
+ * This is the one carrier here that cannot be asked what postage costs. No
340
+ * Royal Mail API quotes rates: not Click & Drop, not Shipping V3, not
341
+ * anything on the developer portal. Click & Drop imports orders and prints
342
+ * labels; postage is charged afterwards at whatever your OBA agreement says.
343
+ *
344
+ * So the shop quotes from this table, and the table has to match the rate
345
+ * card on your account or you will undercharge every order and find out at
346
+ * the invoice. The codes below are real Royal Mail service codes; the prices
347
+ * are NOT — they are placeholders, in pence.
348
+ */
349
+ const ROYAL_MAIL_SERVICES = (): string => `
350
+ /**
351
+ * What this shop charges for Royal Mail, by total parcel weight.
352
+ *
353
+ * TODO VERIFY: every \`amount\` below is a placeholder in pence. Replace them
354
+ * with your own OBA rate card — Click & Drop → Settings → Shipping services
355
+ * lists exactly which services your account has, and the codes here must be
356
+ * among them. Service codes are account-specific: the published list is from
357
+ * 2021 and Royal Mail warn that "available services are unique to your OBA
358
+ * account and individual service agreements".
359
+ *
360
+ * The band whose \`maxGrams\` first covers the parcel wins, so order does not
361
+ * matter. A parcel heavier than every band gets no rate at all rather than a
362
+ * wrong one, and Click & Drop refuses anything over 30 kg.
363
+ */
364
+ export const royalMailServices: RoyalMailService[] = [
365
+ {
366
+ code: "TPN24",
367
+ name: "Tracked 24",
368
+ estimatedDays: 1,
369
+ bands: [
370
+ { maxGrams: 1000, amount: 0 /* TODO VERIFY */ },
371
+ { maxGrams: 2000, amount: 0 /* TODO VERIFY */ },
372
+ { maxGrams: 5000, amount: 0 /* TODO VERIFY */ },
373
+ ],
374
+ },
375
+ {
376
+ code: "TPS48",
377
+ name: "Tracked 48",
378
+ estimatedDays: 2,
379
+ bands: [
380
+ { maxGrams: 1000, amount: 0 /* TODO VERIFY */ },
381
+ { maxGrams: 2000, amount: 0 /* TODO VERIFY */ },
382
+ { maxGrams: 5000, amount: 0 /* TODO VERIFY */ },
383
+ ],
384
+ },
385
+ ];
386
+ `;