@saastemly/voidcommerce 0.12.0 → 0.14.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 +1 -1
- package/dist/cli.js +4 -4
- package/dist/generate/auth.d.ts +1 -1
- package/dist/{index-ym88atwf.js → index-30y19qz5.js} +1 -1
- package/dist/{index-g0p3kk0f.js → index-jvsednjr.js} +54 -18
- package/dist/{index-a279r74m.js → index-paeg95y4.js} +2 -2
- package/dist/{index-7xwcv2cj.js → index-xyjhy6kp.js} +64 -5
- package/dist/index.js +4 -4
- package/dist/{keys-px5maaj1.js → keys-nntc65pq.js} +3 -3
- package/dist/manifest.js +2 -2
- package/package.json +1 -1
- package/src/catalog.ts +53 -5
- package/src/generate/auth.ts +9 -1
- package/src/generate/index.ts +5 -1
- package/src/generate/support.ts +43 -11
package/dist/catalog.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
runVoid,
|
|
22
22
|
version,
|
|
23
23
|
voidAppsIn
|
|
24
|
-
} from "./index-
|
|
24
|
+
} from "./index-jvsednjr.js";
|
|
25
25
|
import {
|
|
26
26
|
LOCAL_KEY_FILE,
|
|
27
27
|
PRIVATE_KEY_VAR,
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
setVariable,
|
|
51
51
|
variableNames,
|
|
52
52
|
verifyCloudflareToken
|
|
53
|
-
} from "./index-
|
|
53
|
+
} from "./index-paeg95y4.js";
|
|
54
54
|
import {
|
|
55
55
|
LAYOUTS,
|
|
56
56
|
oneOrigin,
|
|
@@ -58,10 +58,10 @@ import {
|
|
|
58
58
|
validate,
|
|
59
59
|
writeManifest,
|
|
60
60
|
zoneOf
|
|
61
|
-
} from "./index-
|
|
61
|
+
} from "./index-30y19qz5.js";
|
|
62
62
|
import {
|
|
63
63
|
GROUPS
|
|
64
|
-
} from "./index-
|
|
64
|
+
} from "./index-xyjhy6kp.js";
|
|
65
65
|
import {
|
|
66
66
|
__require
|
|
67
67
|
} from "./index-0v6na3yp.js";
|
package/dist/generate/auth.d.ts
CHANGED
|
@@ -10,6 +10,6 @@ import { type Manifest } from "../manifest";
|
|
|
10
10
|
* The token needs nothing: no origin, no tariff. Everything else needs one or
|
|
11
11
|
* the other, so the rule is simply "any carrier but the token".
|
|
12
12
|
*/
|
|
13
|
-
export declare const WIRED_CARRIERS: readonly ["royal-mail", "dhl-express", "shippo", "easypost", "ups", "fedex", "nft"];
|
|
13
|
+
export declare const WIRED_CARRIERS: readonly ["parcel2go", "royal-mail", "dhl-express", "shippo", "easypost", "ups", "fedex", "nft"];
|
|
14
14
|
export declare const needsShippingConfig: (id: string) => boolean;
|
|
15
15
|
export declare function renderAuthTs(manifest: Manifest): string;
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
renderEnvLocal,
|
|
12
12
|
renderEnvProduction,
|
|
13
13
|
renderEnvTs
|
|
14
|
-
} from "./index-
|
|
14
|
+
} from "./index-paeg95y4.js";
|
|
15
15
|
import {
|
|
16
16
|
MANIFEST_FILE,
|
|
17
17
|
has,
|
|
@@ -24,10 +24,10 @@ import {
|
|
|
24
24
|
workerHosts,
|
|
25
25
|
writeManifest,
|
|
26
26
|
zone
|
|
27
|
-
} from "./index-
|
|
27
|
+
} from "./index-30y19qz5.js";
|
|
28
28
|
import {
|
|
29
29
|
CHOICES
|
|
30
|
-
} from "./index-
|
|
30
|
+
} from "./index-xyjhy6kp.js";
|
|
31
31
|
import {
|
|
32
32
|
__require
|
|
33
33
|
} from "./index-0v6na3yp.js";
|
|
@@ -50,12 +50,17 @@ var VAT = {
|
|
|
50
50
|
PL: { rate: 2300, name: "VAT" }
|
|
51
51
|
};
|
|
52
52
|
var social = (manifest, id, envPrefix) => has(manifest, id) ? ` ${id}: { clientId: must(env, "${envPrefix}_CLIENT_ID"), clientSecret: must(env, "${envPrefix}_CLIENT_SECRET") },` : null;
|
|
53
|
-
var WIRED_CARRIERS = ["royal-mail", "dhl-express", "shippo", "easypost", "ups", "fedex", "nft"];
|
|
53
|
+
var WIRED_CARRIERS = ["parcel2go", "royal-mail", "dhl-express", "shippo", "easypost", "ups", "fedex", "nft"];
|
|
54
54
|
var needsShippingConfig = (id) => WIRED_CARRIERS.includes(id) && id !== "nft";
|
|
55
55
|
function renderAuthTs(manifest) {
|
|
56
56
|
const { shop } = manifest;
|
|
57
57
|
const lines = [];
|
|
58
58
|
const CARRIER_CALLS = {
|
|
59
|
+
parcel2go: {
|
|
60
|
+
label: "Parcel2Go",
|
|
61
|
+
call: `parcel2goProvider({ clientId: must(env, "PARCEL2GO_CLIENT_ID"), clientSecret: must(env, "PARCEL2GO_CLIENT_SECRET"), from: shipFrom, customer: parcel2goCustomer, currency: "${shop.currency}" })`,
|
|
62
|
+
import: `import { parcel2goProvider } from "@saastemly/better-commerce/providers/parcel2go";`
|
|
63
|
+
},
|
|
59
64
|
"royal-mail": {
|
|
60
65
|
label: "Royal Mail",
|
|
61
66
|
call: `royalMailProvider({ apiKey: must(env, "CLICK_DROP_AUTH_KEY"), services: royalMailServices, currency: "${shop.currency}", carrierName: "Royal Mail" })`,
|
|
@@ -97,7 +102,8 @@ import { mintProvider, destinationFor } from "./lib/mint.ts";`
|
|
|
97
102
|
const carrierImports = carriers.map((id) => CARRIER_CALLS[id].import);
|
|
98
103
|
const shipping = [
|
|
99
104
|
...carriers.some((id) => id !== "nft" && id !== "royal-mail") ? ["shipFrom"] : [],
|
|
100
|
-
...carriers.includes("royal-mail") ? ["royalMailServices"] : []
|
|
105
|
+
...carriers.includes("royal-mail") ? ["royalMailServices"] : [],
|
|
106
|
+
...carriers.includes("parcel2go") ? ["parcel2goCustomer"] : []
|
|
101
107
|
];
|
|
102
108
|
if (shipping.length > 0)
|
|
103
109
|
carrierImports.push(`import { ${shipping.join(", ")} } from "./lib/shipping.ts";`);
|
|
@@ -1372,7 +1378,7 @@ import color from "picocolors";
|
|
|
1372
1378
|
// package.json
|
|
1373
1379
|
var package_default = {
|
|
1374
1380
|
name: "@saastemly/voidcommerce",
|
|
1375
|
-
version: "0.
|
|
1381
|
+
version: "0.14.0",
|
|
1376
1382
|
description: "Void, with a shop in it. `vc init` walks you through Better Auth, betterCommerce and every plugin; everything else passes through to `void`.",
|
|
1377
1383
|
type: "module",
|
|
1378
1384
|
license: "MIT",
|
|
@@ -2223,30 +2229,60 @@ export async function destinationFor(orderId: string): Promise<string | null> {
|
|
|
2223
2229
|
}
|
|
2224
2230
|
function renderShippingTs(manifest) {
|
|
2225
2231
|
const royalMail = has(manifest, "royal-mail");
|
|
2226
|
-
const
|
|
2232
|
+
const parcel2go = has(manifest, "parcel2go");
|
|
2233
|
+
const origin = ["parcel2go", "dhl-express", "shippo", "easypost", "ups", "fedex"].some((id) => has(manifest, id));
|
|
2227
2234
|
const imports = [
|
|
2235
|
+
...origin || parcel2go ? [`import { env } from "void/env";`] : [],
|
|
2228
2236
|
...origin ? [`import type { DeliveryAddress } from "@saastemly/better-commerce/providers/delivery";`] : [],
|
|
2229
2237
|
...royalMail ? [`import type { RoyalMailService } from "@saastemly/better-commerce/providers/royal-mail";`] : []
|
|
2230
2238
|
];
|
|
2231
|
-
const
|
|
2239
|
+
const header = `
|
|
2232
2240
|
/**
|
|
2233
|
-
*
|
|
2241
|
+
* Where parcels come FROM, and who the carrier account belongs to.
|
|
2242
|
+
*
|
|
2243
|
+
* These are CONFIG, not code: they vary per deploy, they are nobody's
|
|
2244
|
+
* business but this shop's, and hard-coding them here would mean a fresh
|
|
2245
|
+
* clone of this repository shipped somebody else's address. So they come
|
|
2246
|
+
* from the environment — declared in env.ts, encrypted in .env.secrets, and
|
|
2247
|
+
* set with \`vc secrets set\`.
|
|
2234
2248
|
*
|
|
2235
|
-
*
|
|
2236
|
-
*
|
|
2249
|
+
* This file's job is to read them and fail loudly when one is missing, which
|
|
2250
|
+
* is the difference between a shop that refuses to quote and a shop that
|
|
2251
|
+
* quotes against the wrong postcode and loses money on every order.
|
|
2237
2252
|
*/
|
|
2253
|
+
const must = (key: string): string => {
|
|
2254
|
+
const value = (env as Record<string, unknown>)[key];
|
|
2255
|
+
if (typeof value !== "string" || !value || value === "unset") {
|
|
2256
|
+
throw new Error(\`shipping: \${key} is not set — \\\`vc secrets set \${key}\\\`\`);
|
|
2257
|
+
}
|
|
2258
|
+
return value;
|
|
2259
|
+
};
|
|
2260
|
+
`;
|
|
2261
|
+
const originBlock = `
|
|
2262
|
+
/** The address every parcel is quoted against, and the door a courier is sent to. */
|
|
2238
2263
|
export const shipFrom: DeliveryAddress = {
|
|
2239
|
-
name: "
|
|
2240
|
-
line1: "
|
|
2241
|
-
city: "
|
|
2242
|
-
postalCode: "
|
|
2264
|
+
name: must("SHIP_FROM_NAME"),
|
|
2265
|
+
line1: must("SHIP_FROM_LINE1"),
|
|
2266
|
+
city: must("SHIP_FROM_CITY"),
|
|
2267
|
+
postalCode: must("SHIP_FROM_POSTCODE"),
|
|
2243
2268
|
country: "${manifest.shop.country}",
|
|
2244
|
-
phone: "
|
|
2269
|
+
phone: must("SHIP_FROM_PHONE"),
|
|
2270
|
+
};
|
|
2271
|
+
`;
|
|
2272
|
+
const parcel2goBlock = `
|
|
2273
|
+
/**
|
|
2274
|
+
* Who the Parcel2Go account belongs to — the ACCOUNT holder, not the buyer.
|
|
2275
|
+
* Parcel2Go books a courier for a named person and rejects an order without.
|
|
2276
|
+
*/
|
|
2277
|
+
export const parcel2goCustomer = {
|
|
2278
|
+
email: must("PARCEL2GO_CUSTOMER_EMAIL"),
|
|
2279
|
+
forename: must("PARCEL2GO_CUSTOMER_FORENAME"),
|
|
2280
|
+
surname: must("PARCEL2GO_CUSTOMER_SURNAME"),
|
|
2245
2281
|
};
|
|
2246
2282
|
`;
|
|
2247
2283
|
return `${imports.join(`
|
|
2248
2284
|
`)}
|
|
2249
|
-
${origin ? originBlock : ""}${royalMail ? ROYAL_MAIL_SERVICES() : ""}`;
|
|
2285
|
+
${origin || parcel2go ? header : ""}${origin ? originBlock : ""}${parcel2go ? parcel2goBlock : ""}${royalMail ? ROYAL_MAIL_SERVICES() : ""}`;
|
|
2250
2286
|
}
|
|
2251
2287
|
function renderErpTs() {
|
|
2252
2288
|
return `import { businessCentralProvider } from "@saastemly/better-commerce/providers/business-central";
|
|
@@ -2437,7 +2473,7 @@ async function generateApi(root, dir, manifest, result, opts) {
|
|
|
2437
2473
|
await put(root, at("lib/mint.ts"), renderMintTs(), result, own);
|
|
2438
2474
|
}
|
|
2439
2475
|
if (WIRED_CARRIERS.some((id) => needsShippingConfig(id) && has(manifest, id))) {
|
|
2440
|
-
await put(root, at("lib/shipping.ts"), renderShippingTs(manifest), result,
|
|
2476
|
+
await put(root, at("lib/shipping.ts"), renderShippingTs(manifest), result, "regenerate");
|
|
2441
2477
|
}
|
|
2442
2478
|
if (has(manifest, "business-central")) {
|
|
2443
2479
|
await put(root, at("lib/erp.ts"), renderErpTs(), result, own);
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
isApex,
|
|
5
5
|
oneOrigin,
|
|
6
6
|
zone
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-30y19qz5.js";
|
|
8
8
|
import {
|
|
9
9
|
__require,
|
|
10
10
|
__toESM
|
|
@@ -389,7 +389,7 @@ async function decryptSecrets(project) {
|
|
|
389
389
|
}
|
|
390
390
|
const expected = committedPublicKey(root);
|
|
391
391
|
if (expected) {
|
|
392
|
-
const { publicKeyFor } = await import("./keys-
|
|
392
|
+
const { publicKeyFor } = await import("./keys-nntc65pq.js");
|
|
393
393
|
const derived = await publicKeyFor(privateKey);
|
|
394
394
|
if (derived && derived.toLowerCase() !== expected.toLowerCase()) {
|
|
395
395
|
return {
|
|
@@ -440,11 +440,70 @@ var CARRIERS = {
|
|
|
440
440
|
intro: "Live rates at checkout, quoted per carrier. Flat-rate shipping needs none of these.",
|
|
441
441
|
kind: "multiselect",
|
|
442
442
|
choices: [
|
|
443
|
-
{
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
443
|
+
{
|
|
444
|
+
id: "parcel2go",
|
|
445
|
+
label: "Parcel2Go — a courier collects from your door",
|
|
446
|
+
hint: "the only one here a private individual can use end to end: live prices, a collection booked by API, and label-free services where the driver brings it. No business account, no volume minimum, no monthly fee",
|
|
447
|
+
recommended: true,
|
|
448
|
+
env: [
|
|
449
|
+
{ key: "PARCEL2GO_CLIENT_ID", breaks: "no rates are quoted and no collection is ever booked", where: "parcel2go.com/myaccount/api → Client Credentials" },
|
|
450
|
+
{ key: "PARCEL2GO_CLIENT_SECRET", breaks: "the same — and Parcel2Go will not show it twice, so keep it when you make it", where: "you choose this value when creating the credentials" },
|
|
451
|
+
{ key: "PARCEL2GO_CUSTOMER_EMAIL", breaks: "Parcel2Go rejects every order: it books a courier for a named person and will not accept one without", where: "the email on your Parcel2Go account" },
|
|
452
|
+
{ key: "PARCEL2GO_CUSTOMER_FORENAME", breaks: "the same" },
|
|
453
|
+
{ key: "PARCEL2GO_CUSTOMER_SURNAME", breaks: "the same" },
|
|
454
|
+
{ key: "SHIP_FROM_NAME", breaks: "a courier has no one to ask for at the door, and most carriers refuse an origin with no name" },
|
|
455
|
+
{ key: "SHIP_FROM_LINE1", breaks: "the courier is sent to an incomplete address" },
|
|
456
|
+
{ key: "SHIP_FROM_CITY", breaks: "the same" },
|
|
457
|
+
{ key: "SHIP_FROM_POSTCODE", breaks: "every rate is quoted against the wrong place, so every order is mispriced" },
|
|
458
|
+
{ key: "SHIP_FROM_PHONE", breaks: "a driver who cannot find the door has no one to ring" }
|
|
459
|
+
]
|
|
460
|
+
},
|
|
461
|
+
{ id: "shippo", label: "Shippo", hint: "many carriers through one API", env: [
|
|
462
|
+
{ key: "SHIP_FROM_NAME", breaks: "a courier has no one to ask for at the door, and most carriers refuse an origin with no name" },
|
|
463
|
+
{ key: "SHIP_FROM_LINE1", breaks: "the courier is sent to an incomplete address" },
|
|
464
|
+
{ key: "SHIP_FROM_CITY", breaks: "the same" },
|
|
465
|
+
{ key: "SHIP_FROM_POSTCODE", breaks: "every rate is quoted against the wrong place, so every order is mispriced" },
|
|
466
|
+
{ key: "SHIP_FROM_PHONE", breaks: "a driver who cannot find the door has no one to ring" },
|
|
467
|
+
{ key: "SHIPPO_API_TOKEN", breaks: "calculated rates fall back to flat" }
|
|
468
|
+
] },
|
|
469
|
+
{ id: "easypost", label: "EasyPost", hint: "the same, US-leaning", env: [
|
|
470
|
+
{ key: "SHIP_FROM_NAME", breaks: "a courier has no one to ask for at the door, and most carriers refuse an origin with no name" },
|
|
471
|
+
{ key: "SHIP_FROM_LINE1", breaks: "the courier is sent to an incomplete address" },
|
|
472
|
+
{ key: "SHIP_FROM_CITY", breaks: "the same" },
|
|
473
|
+
{ key: "SHIP_FROM_POSTCODE", breaks: "every rate is quoted against the wrong place, so every order is mispriced" },
|
|
474
|
+
{ key: "SHIP_FROM_PHONE", breaks: "a driver who cannot find the door has no one to ring" },
|
|
475
|
+
{ key: "EASYPOST_API_KEY", breaks: "calculated rates fall back to flat" }
|
|
476
|
+
] },
|
|
477
|
+
{ id: "ups", label: "UPS", hint: "your own UPS account, for negotiated rates an aggregator cannot see", env: [
|
|
478
|
+
{ key: "SHIP_FROM_NAME", breaks: "a courier has no one to ask for at the door, and most carriers refuse an origin with no name" },
|
|
479
|
+
{ key: "SHIP_FROM_LINE1", breaks: "the courier is sent to an incomplete address" },
|
|
480
|
+
{ key: "SHIP_FROM_CITY", breaks: "the same" },
|
|
481
|
+
{ key: "SHIP_FROM_POSTCODE", breaks: "every rate is quoted against the wrong place, so every order is mispriced" },
|
|
482
|
+
{ key: "SHIP_FROM_PHONE", breaks: "a driver who cannot find the door has no one to ring" },
|
|
483
|
+
{ key: "UPS_CLIENT_ID", breaks: "UPS rates are not quoted" },
|
|
484
|
+
{ key: "UPS_CLIENT_SECRET", breaks: "the same" },
|
|
485
|
+
{ key: "UPS_ACCOUNT_NUMBER", breaks: "UPS has no account to bill the transport to" }
|
|
486
|
+
] },
|
|
487
|
+
{ id: "fedex", label: "FedEx", hint: "your own FedEx account, for negotiated rates an aggregator cannot see", env: [
|
|
488
|
+
{ key: "SHIP_FROM_NAME", breaks: "a courier has no one to ask for at the door, and most carriers refuse an origin with no name" },
|
|
489
|
+
{ key: "SHIP_FROM_LINE1", breaks: "the courier is sent to an incomplete address" },
|
|
490
|
+
{ key: "SHIP_FROM_CITY", breaks: "the same" },
|
|
491
|
+
{ key: "SHIP_FROM_POSTCODE", breaks: "every rate is quoted against the wrong place, so every order is mispriced" },
|
|
492
|
+
{ key: "SHIP_FROM_PHONE", breaks: "a driver who cannot find the door has no one to ring" },
|
|
493
|
+
{ key: "FEDEX_CLIENT_ID", breaks: "FedEx rates are not quoted" },
|
|
494
|
+
{ key: "FEDEX_CLIENT_SECRET", breaks: "the same" },
|
|
495
|
+
{ key: "FEDEX_ACCOUNT_NUMBER", breaks: "FedEx has no account to bill the transport to" }
|
|
496
|
+
] },
|
|
497
|
+
{ id: "dhl-express", label: "DHL Express", hint: "international express, and the one worth having for a shop that posts abroad", env: [
|
|
498
|
+
{ key: "SHIP_FROM_NAME", breaks: "a courier has no one to ask for at the door, and most carriers refuse an origin with no name" },
|
|
499
|
+
{ key: "SHIP_FROM_LINE1", breaks: "the courier is sent to an incomplete address" },
|
|
500
|
+
{ key: "SHIP_FROM_CITY", breaks: "the same" },
|
|
501
|
+
{ key: "SHIP_FROM_POSTCODE", breaks: "every rate is quoted against the wrong place, so every order is mispriced" },
|
|
502
|
+
{ key: "SHIP_FROM_PHONE", breaks: "a driver who cannot find the door has no one to ring" },
|
|
503
|
+
{ key: "DHL_API_KEY", breaks: "DHL rates are not quoted" },
|
|
504
|
+
{ key: "DHL_API_SECRET", breaks: "the same" },
|
|
505
|
+
{ key: "DHL_ACCOUNT_NUMBER", breaks: "DHL has no account to bill the transport to" }
|
|
506
|
+
] },
|
|
448
507
|
{ 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
508
|
{
|
|
450
509
|
id: "royal-mail",
|
package/dist/index.js
CHANGED
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
routeProblem,
|
|
53
53
|
strictDependencies,
|
|
54
54
|
upsertJsonc
|
|
55
|
-
} from "./index-
|
|
55
|
+
} from "./index-jvsednjr.js";
|
|
56
56
|
import {
|
|
57
57
|
allEnvKeys,
|
|
58
58
|
envSummary,
|
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
renderEnvLocal,
|
|
61
61
|
renderEnvProduction,
|
|
62
62
|
renderEnvTs
|
|
63
|
-
} from "./index-
|
|
63
|
+
} from "./index-paeg95y4.js";
|
|
64
64
|
import {
|
|
65
65
|
LAYOUTS,
|
|
66
66
|
MANIFEST_FILE,
|
|
@@ -81,7 +81,7 @@ import {
|
|
|
81
81
|
writeManifest,
|
|
82
82
|
zone,
|
|
83
83
|
zoneOf
|
|
84
|
-
} from "./index-
|
|
84
|
+
} from "./index-30y19qz5.js";
|
|
85
85
|
import {
|
|
86
86
|
AUTH_PLUGINS,
|
|
87
87
|
CARRIERS,
|
|
@@ -98,7 +98,7 @@ import {
|
|
|
98
98
|
SIGN_IN,
|
|
99
99
|
TAX,
|
|
100
100
|
UI
|
|
101
|
-
} from "./index-
|
|
101
|
+
} from "./index-xyjhy6kp.js";
|
|
102
102
|
import"./index-0v6na3yp.js";
|
|
103
103
|
export {
|
|
104
104
|
zoneOf,
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
localPrivateKey,
|
|
9
9
|
provisionKey,
|
|
10
10
|
publicKeyFor
|
|
11
|
-
} from "./index-
|
|
12
|
-
import"./index-
|
|
13
|
-
import"./index-
|
|
11
|
+
} from "./index-paeg95y4.js";
|
|
12
|
+
import"./index-30y19qz5.js";
|
|
13
|
+
import"./index-xyjhy6kp.js";
|
|
14
14
|
import"./index-0v6na3yp.js";
|
|
15
15
|
export {
|
|
16
16
|
publicKeyFor,
|
package/dist/manifest.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saastemly/voidcommerce",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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
|
@@ -513,11 +513,59 @@ export const CARRIERS: Group = {
|
|
|
513
513
|
intro: "Live rates at checkout, quoted per carrier. Flat-rate shipping needs none of these.",
|
|
514
514
|
kind: "multiselect",
|
|
515
515
|
choices: [
|
|
516
|
-
{
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
516
|
+
{
|
|
517
|
+
id: "parcel2go",
|
|
518
|
+
label: "Parcel2Go — a courier collects from your door",
|
|
519
|
+
hint: "the only one here a private individual can use end to end: live prices, a collection booked by API, and label-free services where the driver brings it. No business account, no volume minimum, no monthly fee",
|
|
520
|
+
recommended: true,
|
|
521
|
+
env: [
|
|
522
|
+
{ key: "PARCEL2GO_CLIENT_ID", breaks: "no rates are quoted and no collection is ever booked", where: "parcel2go.com/myaccount/api → Client Credentials" },
|
|
523
|
+
{ key: "PARCEL2GO_CLIENT_SECRET", breaks: "the same — and Parcel2Go will not show it twice, so keep it when you make it", where: "you choose this value when creating the credentials" },
|
|
524
|
+
{ key: "PARCEL2GO_CUSTOMER_EMAIL", breaks: "Parcel2Go rejects every order: it books a courier for a named person and will not accept one without", where: "the email on your Parcel2Go account" },
|
|
525
|
+
{ key: "PARCEL2GO_CUSTOMER_FORENAME", breaks: "the same" },
|
|
526
|
+
{ key: "PARCEL2GO_CUSTOMER_SURNAME", breaks: "the same" },
|
|
527
|
+
{ key: "SHIP_FROM_NAME", breaks: "a courier has no one to ask for at the door, and most carriers refuse an origin with no name" },
|
|
528
|
+
{ key: "SHIP_FROM_LINE1", breaks: "the courier is sent to an incomplete address" },
|
|
529
|
+
{ key: "SHIP_FROM_CITY", breaks: "the same" },
|
|
530
|
+
{ key: "SHIP_FROM_POSTCODE", breaks: "every rate is quoted against the wrong place, so every order is mispriced" },
|
|
531
|
+
{ key: "SHIP_FROM_PHONE", breaks: "a driver who cannot find the door has no one to ring" },
|
|
532
|
+
],
|
|
533
|
+
},
|
|
534
|
+
{ id: "shippo", label: "Shippo", hint: "many carriers through one API", env: [
|
|
535
|
+
{ key: "SHIP_FROM_NAME", breaks: "a courier has no one to ask for at the door, and most carriers refuse an origin with no name" },
|
|
536
|
+
{ key: "SHIP_FROM_LINE1", breaks: "the courier is sent to an incomplete address" },
|
|
537
|
+
{ key: "SHIP_FROM_CITY", breaks: "the same" },
|
|
538
|
+
{ key: "SHIP_FROM_POSTCODE", breaks: "every rate is quoted against the wrong place, so every order is mispriced" },
|
|
539
|
+
{ key: "SHIP_FROM_PHONE", breaks: "a driver who cannot find the door has no one to ring" },
|
|
540
|
+
{ key: "SHIPPO_API_TOKEN", breaks: "calculated rates fall back to flat" }] },
|
|
541
|
+
{ id: "easypost", label: "EasyPost", hint: "the same, US-leaning", env: [
|
|
542
|
+
{ key: "SHIP_FROM_NAME", breaks: "a courier has no one to ask for at the door, and most carriers refuse an origin with no name" },
|
|
543
|
+
{ key: "SHIP_FROM_LINE1", breaks: "the courier is sent to an incomplete address" },
|
|
544
|
+
{ key: "SHIP_FROM_CITY", breaks: "the same" },
|
|
545
|
+
{ key: "SHIP_FROM_POSTCODE", breaks: "every rate is quoted against the wrong place, so every order is mispriced" },
|
|
546
|
+
{ key: "SHIP_FROM_PHONE", breaks: "a driver who cannot find the door has no one to ring" },
|
|
547
|
+
{ key: "EASYPOST_API_KEY", breaks: "calculated rates fall back to flat" }] },
|
|
548
|
+
{ id: "ups", label: "UPS", hint: "your own UPS account, for negotiated rates an aggregator cannot see", env: [
|
|
549
|
+
{ key: "SHIP_FROM_NAME", breaks: "a courier has no one to ask for at the door, and most carriers refuse an origin with no name" },
|
|
550
|
+
{ key: "SHIP_FROM_LINE1", breaks: "the courier is sent to an incomplete address" },
|
|
551
|
+
{ key: "SHIP_FROM_CITY", breaks: "the same" },
|
|
552
|
+
{ key: "SHIP_FROM_POSTCODE", breaks: "every rate is quoted against the wrong place, so every order is mispriced" },
|
|
553
|
+
{ key: "SHIP_FROM_PHONE", breaks: "a driver who cannot find the door has no one to ring" },
|
|
554
|
+
{ key: "UPS_CLIENT_ID", breaks: "UPS rates are not quoted" }, { key: "UPS_CLIENT_SECRET", breaks: "the same" }, { key: "UPS_ACCOUNT_NUMBER", breaks: "UPS has no account to bill the transport to" }] },
|
|
555
|
+
{ id: "fedex", label: "FedEx", hint: "your own FedEx account, for negotiated rates an aggregator cannot see", env: [
|
|
556
|
+
{ key: "SHIP_FROM_NAME", breaks: "a courier has no one to ask for at the door, and most carriers refuse an origin with no name" },
|
|
557
|
+
{ key: "SHIP_FROM_LINE1", breaks: "the courier is sent to an incomplete address" },
|
|
558
|
+
{ key: "SHIP_FROM_CITY", breaks: "the same" },
|
|
559
|
+
{ key: "SHIP_FROM_POSTCODE", breaks: "every rate is quoted against the wrong place, so every order is mispriced" },
|
|
560
|
+
{ key: "SHIP_FROM_PHONE", breaks: "a driver who cannot find the door has no one to ring" },
|
|
561
|
+
{ 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" }] },
|
|
562
|
+
{ id: "dhl-express", label: "DHL Express", hint: "international express, and the one worth having for a shop that posts abroad", env: [
|
|
563
|
+
{ key: "SHIP_FROM_NAME", breaks: "a courier has no one to ask for at the door, and most carriers refuse an origin with no name" },
|
|
564
|
+
{ key: "SHIP_FROM_LINE1", breaks: "the courier is sent to an incomplete address" },
|
|
565
|
+
{ key: "SHIP_FROM_CITY", breaks: "the same" },
|
|
566
|
+
{ key: "SHIP_FROM_POSTCODE", breaks: "every rate is quoted against the wrong place, so every order is mispriced" },
|
|
567
|
+
{ key: "SHIP_FROM_PHONE", breaks: "a driver who cannot find the door has no one to ring" },
|
|
568
|
+
{ 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
569
|
{ 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
570
|
{
|
|
523
571
|
id: "royal-mail",
|
package/src/generate/auth.ts
CHANGED
|
@@ -60,7 +60,7 @@ const social = (manifest: Manifest, id: string, envPrefix: string) =>
|
|
|
60
60
|
* The token needs nothing: no origin, no tariff. Everything else needs one or
|
|
61
61
|
* the other, so the rule is simply "any carrier but the token".
|
|
62
62
|
*/
|
|
63
|
-
export const WIRED_CARRIERS = ["royal-mail", "dhl-express", "shippo", "easypost", "ups", "fedex", "nft"] as const;
|
|
63
|
+
export const WIRED_CARRIERS = ["parcel2go", "royal-mail", "dhl-express", "shippo", "easypost", "ups", "fedex", "nft"] as const;
|
|
64
64
|
export const needsShippingConfig = (id: string): boolean => WIRED_CARRIERS.includes(id as never) && id !== "nft";
|
|
65
65
|
|
|
66
66
|
export function renderAuthTs(manifest: Manifest): string {
|
|
@@ -82,6 +82,13 @@ export function renderAuthTs(manifest: Manifest): string {
|
|
|
82
82
|
const CARRIER_CALLS: Record<string, { call: string; label: string; import: string }> = {
|
|
83
83
|
// Every real carrier needs the ORIGIN it ships from, which is shop data
|
|
84
84
|
// rather than a credential — so it lives in an owned lib/shipping.ts.
|
|
85
|
+
parcel2go: {
|
|
86
|
+
label: "Parcel2Go",
|
|
87
|
+
// `from` is the collection address: Parcel2Go requires one on every
|
|
88
|
+
// order, because a courier is being sent to it.
|
|
89
|
+
call: `parcel2goProvider({ clientId: must(env, "PARCEL2GO_CLIENT_ID"), clientSecret: must(env, "PARCEL2GO_CLIENT_SECRET"), from: shipFrom, customer: parcel2goCustomer, currency: "${shop.currency}" })`,
|
|
90
|
+
import: `import { parcel2goProvider } from "@saastemly/better-commerce/providers/parcel2go";`,
|
|
91
|
+
},
|
|
85
92
|
"royal-mail": {
|
|
86
93
|
label: "Royal Mail",
|
|
87
94
|
// No `from`: Click & Drop posts from the account's registered
|
|
@@ -133,6 +140,7 @@ export function renderAuthTs(manifest: Manifest): string {
|
|
|
133
140
|
const shipping = [
|
|
134
141
|
...(carriers.some((id) => id !== "nft" && id !== "royal-mail") ? ["shipFrom"] : []),
|
|
135
142
|
...(carriers.includes("royal-mail") ? ["royalMailServices"] : []),
|
|
143
|
+
...(carriers.includes("parcel2go") ? ["parcel2goCustomer"] : []),
|
|
136
144
|
];
|
|
137
145
|
if (shipping.length > 0) carrierImports.push(`import { ${shipping.join(", ")} } from "./lib/shipping.ts";`);
|
|
138
146
|
const wantsNft = has(manifest, "nft");
|
package/src/generate/index.ts
CHANGED
|
@@ -248,7 +248,11 @@ async function generateApi(root: string, dir: string, manifest: Manifest, result
|
|
|
248
248
|
// against, or a price table. The list lives with the carriers themselves so
|
|
249
249
|
// it cannot drift from them.
|
|
250
250
|
if (WIRED_CARRIERS.some((id) => needsShippingConfig(id) && has(manifest, id))) {
|
|
251
|
-
|
|
251
|
+
// REGENERATED, not owned. It used to carry the shop's address as
|
|
252
|
+
// constants, which made it config and so nobody's to overwrite. Now it
|
|
253
|
+
// only READS that config from the environment, so it is ordinary code
|
|
254
|
+
// and should receive fixes like any other generated file.
|
|
255
|
+
await put(root, at("lib/shipping.ts"), renderShippingTs(manifest), result, "regenerate");
|
|
252
256
|
}
|
|
253
257
|
if (has(manifest, "business-central")) {
|
|
254
258
|
await put(root, at("lib/erp.ts"), renderErpTs(), result, own);
|
package/src/generate/support.ts
CHANGED
|
@@ -275,33 +275,65 @@ export async function destinationFor(orderId: string): Promise<string | null> {
|
|
|
275
275
|
*/
|
|
276
276
|
export function renderShippingTs(manifest: Manifest): string {
|
|
277
277
|
const royalMail = has(manifest, "royal-mail");
|
|
278
|
+
const parcel2go = has(manifest, "parcel2go");
|
|
278
279
|
// Royal Mail posts from the address registered on the Click & Drop account,
|
|
279
280
|
// 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));
|
|
281
|
+
const origin = ["parcel2go", "dhl-express", "shippo", "easypost", "ups", "fedex"].some((id) => has(manifest, id));
|
|
281
282
|
|
|
282
283
|
const imports = [
|
|
284
|
+
...(origin || parcel2go ? [`import { env } from "void/env";`] : []),
|
|
283
285
|
...(origin ? [`import type { DeliveryAddress } from "@saastemly/better-commerce/providers/delivery";`] : []),
|
|
284
286
|
...(royalMail ? [`import type { RoyalMailService } from "@saastemly/better-commerce/providers/royal-mail";`] : []),
|
|
285
287
|
];
|
|
286
288
|
|
|
287
|
-
const
|
|
289
|
+
const header = `
|
|
288
290
|
/**
|
|
289
|
-
*
|
|
291
|
+
* Where parcels come FROM, and who the carrier account belongs to.
|
|
292
|
+
*
|
|
293
|
+
* These are CONFIG, not code: they vary per deploy, they are nobody's
|
|
294
|
+
* business but this shop's, and hard-coding them here would mean a fresh
|
|
295
|
+
* clone of this repository shipped somebody else's address. So they come
|
|
296
|
+
* from the environment — declared in env.ts, encrypted in .env.secrets, and
|
|
297
|
+
* set with \`vc secrets set\`.
|
|
290
298
|
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
299
|
+
* This file's job is to read them and fail loudly when one is missing, which
|
|
300
|
+
* is the difference between a shop that refuses to quote and a shop that
|
|
301
|
+
* quotes against the wrong postcode and loses money on every order.
|
|
293
302
|
*/
|
|
303
|
+
const must = (key: string): string => {
|
|
304
|
+
const value = (env as Record<string, unknown>)[key];
|
|
305
|
+
if (typeof value !== "string" || !value || value === "unset") {
|
|
306
|
+
throw new Error(\`shipping: \${key} is not set — \\\`vc secrets set \${key}\\\`\`);
|
|
307
|
+
}
|
|
308
|
+
return value;
|
|
309
|
+
};
|
|
310
|
+
`;
|
|
311
|
+
|
|
312
|
+
const originBlock = `
|
|
313
|
+
/** The address every parcel is quoted against, and the door a courier is sent to. */
|
|
294
314
|
export const shipFrom: DeliveryAddress = {
|
|
295
|
-
name: "
|
|
296
|
-
line1: "
|
|
297
|
-
city: "
|
|
298
|
-
postalCode: "
|
|
315
|
+
name: must("SHIP_FROM_NAME"),
|
|
316
|
+
line1: must("SHIP_FROM_LINE1"),
|
|
317
|
+
city: must("SHIP_FROM_CITY"),
|
|
318
|
+
postalCode: must("SHIP_FROM_POSTCODE"),
|
|
299
319
|
country: "${manifest.shop.country}",
|
|
300
|
-
phone: "
|
|
320
|
+
phone: must("SHIP_FROM_PHONE"),
|
|
321
|
+
};
|
|
322
|
+
`;
|
|
323
|
+
|
|
324
|
+
const parcel2goBlock = `
|
|
325
|
+
/**
|
|
326
|
+
* Who the Parcel2Go account belongs to — the ACCOUNT holder, not the buyer.
|
|
327
|
+
* Parcel2Go books a courier for a named person and rejects an order without.
|
|
328
|
+
*/
|
|
329
|
+
export const parcel2goCustomer = {
|
|
330
|
+
email: must("PARCEL2GO_CUSTOMER_EMAIL"),
|
|
331
|
+
forename: must("PARCEL2GO_CUSTOMER_FORENAME"),
|
|
332
|
+
surname: must("PARCEL2GO_CUSTOMER_SURNAME"),
|
|
301
333
|
};
|
|
302
334
|
`;
|
|
303
335
|
|
|
304
|
-
return `${imports.join("\n")}\n${origin ? originBlock : ""}${royalMail ? ROYAL_MAIL_SERVICES() : ""}`;
|
|
336
|
+
return `${imports.join("\n")}\n${origin || parcel2go ? header : ""}${origin ? originBlock : ""}${parcel2go ? parcel2goBlock : ""}${royalMail ? ROYAL_MAIL_SERVICES() : ""}`;
|
|
305
337
|
}
|
|
306
338
|
|
|
307
339
|
export function renderErpTs(): string {
|