@odoro-cli/server-commerce 0.4.0 → 0.5.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/README.md +3 -3
- package/dist/index.d.ts +92 -2
- package/dist/index.js +274 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,10 +33,10 @@ createApp({
|
|
|
33
33
|
- discount codes (shop 1.2.0): a percentage or an amount off the basket, priced by the server from `shop.discount_codes` — dates, active flag, minimum basket and usage cap checked; a use counts once the order is paid; once the payment is open, the code is frozen. On a shop still in 1.1.0, a code is refused by name;
|
|
34
34
|
- `POST /api/storefront/payment-callback`: the order becomes paid only on a callback signed with `callbackSecret` (HMAC-SHA256 of `reference.state.timestamp`, 5-minute window). Idempotent: the stock leaves once.
|
|
35
35
|
|
|
36
|
-
##
|
|
36
|
+
## The customer account (shop 1.3.0)
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
With a `mail` port (`odoroMail`: Odoro sends the link, the shop holds no mail key), `/api/storefront/account` serves the central storefront's contract: `lien` (a fifteen-minute link, at most three an hour, the answer never says whether the address is known), `lettre` (only with the ticked box), `profil` (session required; the consent can be withdrawn), `deconnexion`, and `GET` (profile, paid orders found by address, addresses). The link and the session live in the database as SHA-256 fingerprints only. The link is opened by `openSession`, which the app mounts on `/api/storefront/account/login` (a redirect with cookies, not JSON). Without the port, or on a shop before 1.3.0, the account says it is not available, by name.
|
|
39
39
|
|
|
40
40
|
## Tests
|
|
41
41
|
|
|
42
|
-
`COMMERCE_TEST_URL=postgres://user@host:port/postgres pnpm test` — each run creates a database, loads `test/fixtures/shop-1.
|
|
42
|
+
`COMMERCE_TEST_URL=postgres://user@host:port/postgres pnpm test` — each run creates a database, loads `test/fixtures/shop-1.3.0.sql`, and drops it.
|
package/dist/index.d.ts
CHANGED
|
@@ -117,6 +117,63 @@ declare function verifyCallback(secret: string, input: {
|
|
|
117
117
|
signature: string;
|
|
118
118
|
}, now?: number): boolean;
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The customer's account, in the shop's own database (shop 1.3.0).
|
|
122
|
+
*
|
|
123
|
+
* No password: a fifteen-minute link, sent to the address. The link and the
|
|
124
|
+
* session live in the database as FINGERPRINTS only (SHA-256): a database
|
|
125
|
+
* that is read connects nobody.
|
|
126
|
+
*
|
|
127
|
+
* The same contract as Odoro's central storefront, so the site's account
|
|
128
|
+
* page works unchanged:
|
|
129
|
+
*
|
|
130
|
+
* POST { geste: 'lien', email, offres? } create the account / sign in:
|
|
131
|
+
* a link leaves by e-mail;
|
|
132
|
+
* POST { geste: 'lettre', email, offres } the newsletter — `offres` MUST be
|
|
133
|
+
* true (the ticked box);
|
|
134
|
+
* POST { geste: 'profil', nom?, offres? } the profile (session required);
|
|
135
|
+
* POST { geste: 'deconnexion' } closes the session;
|
|
136
|
+
* GET the profile, the orders, the addresses.
|
|
137
|
+
*
|
|
138
|
+
* `lien` and `lettre` always answer `{ ok: true }`: the form must not become
|
|
139
|
+
* a directory of the shop's customers. At most three links per person per
|
|
140
|
+
* hour: without that bound, the page would fill someone's mailbox.
|
|
141
|
+
*
|
|
142
|
+
* The link itself is opened by `openSession` — mounted by the app on
|
|
143
|
+
* `/api/storefront/account/login`, because it answers with a redirect and
|
|
144
|
+
* cookies, not with JSON.
|
|
145
|
+
*
|
|
146
|
+
* @module
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
/** The session cookie: HttpOnly, the browser's script never reads it. */
|
|
150
|
+
declare const CUSTOMER_COOKIE = "odoro_client";
|
|
151
|
+
/** How long a session lasts, in seconds. */
|
|
152
|
+
declare const SESSION_MAX_AGE: number;
|
|
153
|
+
/** Links per person per hour. */
|
|
154
|
+
declare const LINKS_PER_HOUR = 3;
|
|
155
|
+
/** Where the sign-in link leaves: Odoro sends it, the shop holds no mail key. */
|
|
156
|
+
interface MailPort {
|
|
157
|
+
sendLoginLink(input: {
|
|
158
|
+
readonly email: string;
|
|
159
|
+
readonly token: string;
|
|
160
|
+
}): Promise<void>;
|
|
161
|
+
}
|
|
162
|
+
/** Are the accounts installed (shop 1.3.0)? */
|
|
163
|
+
declare function accountsInstalled(db: Query): Promise<boolean>;
|
|
164
|
+
/**
|
|
165
|
+
* The link from the e-mail, opened: it serves ONCE. Returns the session to
|
|
166
|
+
* set, and the cart to set when the person had one elsewhere — or `null`
|
|
167
|
+
* when the link is unknown, used or expired.
|
|
168
|
+
*
|
|
169
|
+
* The cart follows the person: this browser's cart becomes theirs; if this
|
|
170
|
+
* browser has none, their last open cart comes back.
|
|
171
|
+
*/
|
|
172
|
+
declare function openSession(db: Base, token: string, cartToken: string | undefined): Promise<{
|
|
173
|
+
session: string;
|
|
174
|
+
cart: string | null;
|
|
175
|
+
} | null>;
|
|
176
|
+
|
|
120
177
|
interface CommerceOptions {
|
|
121
178
|
/** The site's shop database (`@odoro-cli/cloud-connect`, or any `Base`). */
|
|
122
179
|
readonly db: Base;
|
|
@@ -126,11 +183,44 @@ interface CommerceOptions {
|
|
|
126
183
|
readonly callbackSecret: string;
|
|
127
184
|
/** The shop's currency. @defaultValue 'EUR' */
|
|
128
185
|
readonly currency?: string;
|
|
129
|
-
/** Mark the cart
|
|
186
|
+
/** Mark the cart and session cookies `Secure`. @defaultValue true */
|
|
130
187
|
readonly secureCookie?: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* Where the sign-in links leave: Odoro sends them. Without it, the account
|
|
190
|
+
* says it is not available — by name.
|
|
191
|
+
*/
|
|
192
|
+
readonly mail?: MailPort;
|
|
131
193
|
}
|
|
132
194
|
declare function createCommerceModule(options: CommerceOptions): _odoro_cli_server.ModuleDefinition<Record<never, never>>;
|
|
133
195
|
|
|
196
|
+
/**
|
|
197
|
+
* The sign-in link, sent by Odoro.
|
|
198
|
+
*
|
|
199
|
+
* A shop holds no mail key: it asks Odoro to send the link, signed with the
|
|
200
|
+
* site's secret exactly like a payment (`odoro-payment.ts`). Odoro writes the
|
|
201
|
+
* address of the link itself — the shop's own published address — and only
|
|
202
|
+
* takes the TOKEN from here: a compromised site cannot make Odoro mail a link
|
|
203
|
+
* that leads anywhere else.
|
|
204
|
+
*
|
|
205
|
+
* @module
|
|
206
|
+
*/
|
|
207
|
+
|
|
208
|
+
interface OdoroMailOptions {
|
|
209
|
+
/** Odoro's address, e.g. `https://odoro.ai`. */
|
|
210
|
+
readonly origin: string;
|
|
211
|
+
/** This site's identifier at Odoro. */
|
|
212
|
+
readonly site: string;
|
|
213
|
+
/** This site's secret, shared with Odoro alone. */
|
|
214
|
+
readonly secret: string;
|
|
215
|
+
/** @internal For the tests. */
|
|
216
|
+
readonly fetch?: typeof fetch;
|
|
217
|
+
/** @internal For the tests: seconds since the epoch. */
|
|
218
|
+
readonly now?: () => number;
|
|
219
|
+
}
|
|
220
|
+
/** Where Odoro sends a site's sign-in link. */
|
|
221
|
+
declare const ODORO_MAIL_PATH = "/api/hosting/site-login-link";
|
|
222
|
+
declare function odoroMail(options: OdoroMailOptions): MailPort;
|
|
223
|
+
|
|
134
224
|
/**
|
|
135
225
|
* The payment, opened at Odoro.
|
|
136
226
|
*
|
|
@@ -219,4 +309,4 @@ interface StoredImage {
|
|
|
219
309
|
*/
|
|
220
310
|
declare function productImage(db: Query, productId: string): Promise<StoredImage | null>;
|
|
221
311
|
|
|
222
|
-
export { type Base, CART_COOKIE, type CheckoutInput, type CommerceOptions, ODORO_PAYMENT_PATH, type OdoroPaymentOptions, type PaymentPort, type PgClientLike, type PgPoolLike, type Query, SHOP_MAJOR, type StoredImage, baseFromPool, codesInstalled, createCommerceModule, discountFor, odoroPayment, productImage, shopInstalled, signCallback, signRequest, storageInstalled, verifyCallback };
|
|
312
|
+
export { type Base, CART_COOKIE, CUSTOMER_COOKIE, type CheckoutInput, type CommerceOptions, LINKS_PER_HOUR, type MailPort, ODORO_MAIL_PATH, ODORO_PAYMENT_PATH, type OdoroMailOptions, type OdoroPaymentOptions, type PaymentPort, type PgClientLike, type PgPoolLike, type Query, SESSION_MAX_AGE, SHOP_MAJOR, type StoredImage, accountsInstalled, baseFromPool, codesInstalled, createCommerceModule, discountFor, odoroMail, odoroPayment, openSession, productImage, shopInstalled, signCallback, signRequest, storageInstalled, verifyCallback };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHmac, timingSafeEqual, randomBytes } from 'crypto';
|
|
1
|
+
import { createHmac, timingSafeEqual, createHash, randomBytes } from 'crypto';
|
|
2
2
|
import { route, defineModule, NotFoundError, ConflictError, ServiceUnavailableError, ApiError } from '@odoro-cli/server';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
@@ -394,7 +394,7 @@ async function openCheckout(db, cartId, input, currency = "EUR") {
|
|
|
394
394
|
if (!EMAIL.test(email))
|
|
395
395
|
throw new CheckoutError(400, "Cette adresse e-mail n'est pas valable.");
|
|
396
396
|
const name = need(input.nom, "Indiquez votre nom.");
|
|
397
|
-
const
|
|
397
|
+
const address2 = {
|
|
398
398
|
ligne1: need(input.ligne1, "Indiquez votre adresse."),
|
|
399
399
|
ligne2: (input.ligne2 ?? "").trim(),
|
|
400
400
|
code_postal: need(input.code_postal, "Indiquez votre code postal."),
|
|
@@ -465,7 +465,7 @@ async function openCheckout(db, cartId, input, currency = "EUR") {
|
|
|
465
465
|
const { rows: created } = await tx.query(
|
|
466
466
|
`INSERT INTO shop.orders (cart_id, email, customer_name, shipping_address, currency, subtotal_cents, total_cents)
|
|
467
467
|
VALUES ($1, $2, $3, $4, $5, $6, $6) RETURNING id`,
|
|
468
|
-
[cartId, email, name, JSON.stringify(
|
|
468
|
+
[cartId, email, name, JSON.stringify(address2), currency, subtotal]
|
|
469
469
|
);
|
|
470
470
|
const orderId = created[0].id;
|
|
471
471
|
for (const l of lines) {
|
|
@@ -677,6 +677,195 @@ async function orderState(db, orderId) {
|
|
|
677
677
|
livraisons: []
|
|
678
678
|
};
|
|
679
679
|
}
|
|
680
|
+
var CUSTOMER_COOKIE = "odoro_client";
|
|
681
|
+
var SESSION_MAX_AGE = 90 * 24 * 3600;
|
|
682
|
+
var LINK_MINUTES = 15;
|
|
683
|
+
var LINKS_PER_HOUR = 3;
|
|
684
|
+
var TOKEN2 = /^[A-Za-z0-9_-]{20,80}$/;
|
|
685
|
+
var EMAIL2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
686
|
+
async function accountsInstalled(db) {
|
|
687
|
+
try {
|
|
688
|
+
const { rows } = await db.query(
|
|
689
|
+
`SELECT to_regclass('shop.customers') IS NOT NULL AS present`
|
|
690
|
+
);
|
|
691
|
+
return rows[0]?.present === true;
|
|
692
|
+
} catch {
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function fingerprint(token) {
|
|
697
|
+
return createHash("sha256").update(token).digest("hex");
|
|
698
|
+
}
|
|
699
|
+
function newToken() {
|
|
700
|
+
return randomBytes(32).toString("base64url");
|
|
701
|
+
}
|
|
702
|
+
async function register(db, email, input) {
|
|
703
|
+
const { rows } = await db.query(
|
|
704
|
+
`INSERT INTO shop.customers (email, name, marketing_consent, consent_source, consent_at)
|
|
705
|
+
VALUES ($1, $2, $3, CASE WHEN $3 THEN $4 END, CASE WHEN $3 THEN now() END)
|
|
706
|
+
ON CONFLICT (email) DO UPDATE SET
|
|
707
|
+
name = CASE WHEN excluded.name <> '' THEN excluded.name ELSE shop.customers.name END,
|
|
708
|
+
marketing_consent = shop.customers.marketing_consent OR excluded.marketing_consent,
|
|
709
|
+
consent_source = CASE WHEN excluded.marketing_consent AND NOT shop.customers.marketing_consent
|
|
710
|
+
THEN excluded.consent_source ELSE shop.customers.consent_source END,
|
|
711
|
+
consent_at = CASE WHEN excluded.marketing_consent AND NOT shop.customers.marketing_consent
|
|
712
|
+
THEN now() ELSE shop.customers.consent_at END
|
|
713
|
+
RETURNING id`,
|
|
714
|
+
[email, (input.name ?? "").trim().slice(0, 120), input.consent, input.source]
|
|
715
|
+
);
|
|
716
|
+
return rows[0].id;
|
|
717
|
+
}
|
|
718
|
+
function address(raw) {
|
|
719
|
+
const email = (raw ?? "").trim().toLowerCase().slice(0, 254);
|
|
720
|
+
if (!EMAIL2.test(email))
|
|
721
|
+
throw new CheckoutError(400, "Cette adresse de courriel n'est pas valable.");
|
|
722
|
+
return email;
|
|
723
|
+
}
|
|
724
|
+
async function requestLink(db, mail, input) {
|
|
725
|
+
const email = address(input.email);
|
|
726
|
+
const id = await register(db, email, {
|
|
727
|
+
...input.name === void 0 ? {} : { name: input.name },
|
|
728
|
+
consent: input.consent === true,
|
|
729
|
+
source: "compte"
|
|
730
|
+
});
|
|
731
|
+
const { rows } = await db.query(
|
|
732
|
+
`SELECT count(*)::integer AS n FROM shop.customer_links
|
|
733
|
+
WHERE customer_id = $1 AND created_at > now() - interval '1 hour'`,
|
|
734
|
+
[id]
|
|
735
|
+
);
|
|
736
|
+
if (Number(rows[0]?.n ?? 0) >= LINKS_PER_HOUR) return;
|
|
737
|
+
const token = newToken();
|
|
738
|
+
await db.query(
|
|
739
|
+
`INSERT INTO shop.customer_links (customer_id, fingerprint, expires_at)
|
|
740
|
+
VALUES ($1, $2, now() + make_interval(mins => $3))`,
|
|
741
|
+
[id, fingerprint(token), LINK_MINUTES]
|
|
742
|
+
);
|
|
743
|
+
try {
|
|
744
|
+
await mail.sendLoginLink({ email, token });
|
|
745
|
+
} catch {
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
async function subscribe(db, input) {
|
|
749
|
+
if (input.consent !== true) throw new CheckoutError(400, "Requ\xEAte illisible.");
|
|
750
|
+
await register(db, address(input.email), {
|
|
751
|
+
...input.name === void 0 ? {} : { name: input.name },
|
|
752
|
+
consent: true,
|
|
753
|
+
source: "lettre"
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
async function openSession(db, token, cartToken) {
|
|
757
|
+
if (!TOKEN2.test(token)) return null;
|
|
758
|
+
return await db.transaction(async (tx) => {
|
|
759
|
+
const { rows } = await tx.query(
|
|
760
|
+
`UPDATE shop.customer_links SET used_at = now()
|
|
761
|
+
WHERE fingerprint = $1 AND used_at IS NULL AND expires_at > now()
|
|
762
|
+
RETURNING customer_id`,
|
|
763
|
+
[fingerprint(token)]
|
|
764
|
+
);
|
|
765
|
+
const customer = rows[0]?.customer_id;
|
|
766
|
+
if (customer === void 0) return null;
|
|
767
|
+
const session = newToken();
|
|
768
|
+
await tx.query(
|
|
769
|
+
`INSERT INTO shop.customer_sessions (customer_id, fingerprint, expires_at)
|
|
770
|
+
VALUES ($1, $2, now() + make_interval(secs => $3))`,
|
|
771
|
+
[customer, fingerprint(session), SESSION_MAX_AGE]
|
|
772
|
+
);
|
|
773
|
+
await tx.query("UPDATE shop.customers SET last_seen_at = now() WHERE id = $1", [
|
|
774
|
+
customer
|
|
775
|
+
]);
|
|
776
|
+
if (cartToken !== void 0 && cartToken !== "") {
|
|
777
|
+
const { rows: mine } = await tx.query(
|
|
778
|
+
`UPDATE shop.carts SET customer_id = $2, updated_at = now()
|
|
779
|
+
WHERE token = $1 AND state = 'ouvert' RETURNING id`,
|
|
780
|
+
[cartToken, customer]
|
|
781
|
+
);
|
|
782
|
+
if (mine.length > 0) return { session, cart: null };
|
|
783
|
+
}
|
|
784
|
+
const { rows: last } = await tx.query(
|
|
785
|
+
`SELECT token FROM shop.carts
|
|
786
|
+
WHERE customer_id = $1 AND state = 'ouvert'
|
|
787
|
+
ORDER BY updated_at DESC LIMIT 1`,
|
|
788
|
+
[customer]
|
|
789
|
+
);
|
|
790
|
+
return { session, cart: last[0]?.token ?? null };
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
async function customerOf(db, session) {
|
|
794
|
+
if (session === void 0 || !TOKEN2.test(session)) return null;
|
|
795
|
+
const { rows } = await db.query(
|
|
796
|
+
`SELECT c.id, c.email, c.name, c.marketing_consent
|
|
797
|
+
FROM shop.customer_sessions s JOIN shop.customers c ON c.id = s.customer_id
|
|
798
|
+
WHERE s.fingerprint = $1 AND s.expires_at > now()`,
|
|
799
|
+
[fingerprint(session)]
|
|
800
|
+
);
|
|
801
|
+
const c = rows[0];
|
|
802
|
+
return c === void 0 ? null : { id: c.id, email: c.email, name: c.name, consent: c.marketing_consent };
|
|
803
|
+
}
|
|
804
|
+
async function profile(db, customer) {
|
|
805
|
+
const { rows: orders } = await db.query(
|
|
806
|
+
`SELECT id, state, total_cents, currency, created_at, paid_at, shipping_address, customer_name
|
|
807
|
+
FROM shop.orders
|
|
808
|
+
WHERE lower(email) = $1 AND state IN ('payee', 'remboursee', 'annulee')
|
|
809
|
+
ORDER BY created_at DESC LIMIT 50`,
|
|
810
|
+
[customer.email]
|
|
811
|
+
);
|
|
812
|
+
const seen = /* @__PURE__ */ new Set();
|
|
813
|
+
const addresses = [];
|
|
814
|
+
for (const o of orders) {
|
|
815
|
+
const a = o.shipping_address ?? {};
|
|
816
|
+
const lignes = [
|
|
817
|
+
a["ligne1"],
|
|
818
|
+
a["ligne2"],
|
|
819
|
+
`${a["code_postal"] ?? ""} ${a["ville"] ?? ""}`.trim(),
|
|
820
|
+
a["pays"]
|
|
821
|
+
].filter((x) => typeof x === "string" && x !== "");
|
|
822
|
+
const key = lignes.join("|");
|
|
823
|
+
if (lignes.length === 0 || seen.has(key) || addresses.length >= 5) continue;
|
|
824
|
+
seen.add(key);
|
|
825
|
+
addresses.push({ nom: o.customer_name || null, lignes });
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
nom: customer.name === "" ? null : customer.name,
|
|
829
|
+
courriel: customer.email,
|
|
830
|
+
offres: customer.consent,
|
|
831
|
+
commandes: orders.map((o) => ({
|
|
832
|
+
id: o.id,
|
|
833
|
+
etat: o.state,
|
|
834
|
+
total: new Intl.NumberFormat("fr", {
|
|
835
|
+
style: "currency",
|
|
836
|
+
currency: o.currency
|
|
837
|
+
}).format(Number(o.total_cents) / 100),
|
|
838
|
+
le: new Date(o.paid_at ?? o.created_at).toISOString()
|
|
839
|
+
})),
|
|
840
|
+
adresses: addresses
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
async function updateProfile(db, customer, input) {
|
|
844
|
+
await db.query(
|
|
845
|
+
`UPDATE shop.customers SET
|
|
846
|
+
name = coalesce($2, name),
|
|
847
|
+
marketing_consent = coalesce($3, marketing_consent),
|
|
848
|
+
consent_source = CASE WHEN $3 IS TRUE AND NOT marketing_consent THEN 'compte'
|
|
849
|
+
WHEN $3 IS FALSE THEN NULL ELSE consent_source END,
|
|
850
|
+
consent_at = CASE WHEN $3 IS TRUE AND NOT marketing_consent THEN now()
|
|
851
|
+
WHEN $3 IS FALSE THEN NULL ELSE consent_at END,
|
|
852
|
+
last_seen_at = now()
|
|
853
|
+
WHERE id = $1`,
|
|
854
|
+
[
|
|
855
|
+
customer.id,
|
|
856
|
+
input.name === void 0 ? null : input.name.trim().slice(0, 120),
|
|
857
|
+
input.consent === void 0 ? null : input.consent
|
|
858
|
+
]
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
async function closeSession(db, session) {
|
|
862
|
+
if (session === void 0 || !TOKEN2.test(session)) return;
|
|
863
|
+
await db.query("DELETE FROM shop.customer_sessions WHERE fingerprint = $1", [
|
|
864
|
+
fingerprint(session)
|
|
865
|
+
]);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/module.ts
|
|
680
869
|
function refusal(kind, message) {
|
|
681
870
|
const extensions = { extensions: { erreur: message } };
|
|
682
871
|
if (kind === "NOT_FOUND") return new NotFoundError(message, extensions);
|
|
@@ -904,19 +1093,65 @@ function createCommerceModule(options) {
|
|
|
904
1093
|
method: "GET",
|
|
905
1094
|
path: "/api/storefront/account",
|
|
906
1095
|
auth: "public",
|
|
907
|
-
handler:
|
|
1096
|
+
handler: async ({ cookies }) => {
|
|
1097
|
+
if (options.mail === void 0 || !await accountsInstalled(db))
|
|
1098
|
+
return { connecte: false };
|
|
1099
|
+
const customer = await customerOf(db, cookies.get(CUSTOMER_COOKIE));
|
|
1100
|
+
if (customer === null) return { connecte: false };
|
|
1101
|
+
return { connecte: true, profil: await profile(db, customer) };
|
|
1102
|
+
}
|
|
908
1103
|
});
|
|
909
1104
|
const accountWrite = route({
|
|
910
1105
|
name: "storefront.account.write",
|
|
911
1106
|
method: "POST",
|
|
912
1107
|
path: "/api/storefront/account",
|
|
913
1108
|
auth: "public",
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
)
|
|
919
|
-
}
|
|
1109
|
+
input: z.object({
|
|
1110
|
+
geste: z.enum(["lien", "lettre", "profil", "deconnexion"]).optional(),
|
|
1111
|
+
email: z.string().max(254).optional(),
|
|
1112
|
+
offres: z.boolean().optional(),
|
|
1113
|
+
nom: z.string().max(120).optional()
|
|
1114
|
+
}),
|
|
1115
|
+
handler: async ({ input, cookies }) => await speaking(async () => {
|
|
1116
|
+
const mail = options.mail;
|
|
1117
|
+
if (mail === void 0 || !await accountsInstalled(db)) {
|
|
1118
|
+
throw refusal(
|
|
1119
|
+
"UNAVAILABLE",
|
|
1120
|
+
"Le compte client n'est pas encore disponible sur cette boutique."
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
const geste = input.geste ?? "lien";
|
|
1124
|
+
const who = {
|
|
1125
|
+
email: input.email,
|
|
1126
|
+
...input.nom === void 0 ? {} : { name: input.nom },
|
|
1127
|
+
...input.offres === void 0 ? {} : { consent: input.offres }
|
|
1128
|
+
};
|
|
1129
|
+
if (geste === "lien") {
|
|
1130
|
+
await requestLink(db, mail, who);
|
|
1131
|
+
return { ok: true };
|
|
1132
|
+
}
|
|
1133
|
+
if (geste === "lettre") {
|
|
1134
|
+
await subscribe(db, who);
|
|
1135
|
+
return { ok: true };
|
|
1136
|
+
}
|
|
1137
|
+
const session = cookies.get(CUSTOMER_COOKIE);
|
|
1138
|
+
if (geste === "deconnexion") {
|
|
1139
|
+
await closeSession(db, session);
|
|
1140
|
+
cookies.clear(CUSTOMER_COOKIE);
|
|
1141
|
+
return { ok: true };
|
|
1142
|
+
}
|
|
1143
|
+
const customer = await customerOf(db, session);
|
|
1144
|
+
if (customer === null) {
|
|
1145
|
+
const message = "Ce lien de connexion est p\xE9rim\xE9 ou a d\xE9j\xE0 servi. Demandez-en un nouveau.";
|
|
1146
|
+
throw new ApiError("UNAUTHORIZED", message, { extensions: { erreur: message } });
|
|
1147
|
+
}
|
|
1148
|
+
await updateProfile(db, customer, {
|
|
1149
|
+
...input.nom === void 0 ? {} : { name: input.nom },
|
|
1150
|
+
...input.offres === void 0 ? {} : { consent: input.offres }
|
|
1151
|
+
});
|
|
1152
|
+
const after = await customerOf(db, session) ?? customer;
|
|
1153
|
+
return { ok: true, profil: await profile(db, after) };
|
|
1154
|
+
})
|
|
920
1155
|
});
|
|
921
1156
|
return defineModule({
|
|
922
1157
|
name: "commerce",
|
|
@@ -940,7 +1175,7 @@ var UNAVAILABLE = "Le paiement n'a pas pu s'ouvrir. R\xE9essayez dans un instant
|
|
|
940
1175
|
function odoroPayment(options) {
|
|
941
1176
|
const send = options.fetch ?? fetch;
|
|
942
1177
|
const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
943
|
-
const
|
|
1178
|
+
const address2 = new URL(ODORO_PAYMENT_PATH, options.origin).toString();
|
|
944
1179
|
return {
|
|
945
1180
|
open: async (order) => {
|
|
946
1181
|
if (options.secret === "") throw new CheckoutError(409, UNAVAILABLE);
|
|
@@ -955,7 +1190,7 @@ function odoroPayment(options) {
|
|
|
955
1190
|
const timestamp = now();
|
|
956
1191
|
let reply;
|
|
957
1192
|
try {
|
|
958
|
-
reply = await send(
|
|
1193
|
+
reply = await send(address2, {
|
|
959
1194
|
method: "POST",
|
|
960
1195
|
headers: {
|
|
961
1196
|
"content-type": "application/json",
|
|
@@ -978,6 +1213,32 @@ function odoroPayment(options) {
|
|
|
978
1213
|
};
|
|
979
1214
|
}
|
|
980
1215
|
|
|
1216
|
+
// src/odoro-mail.ts
|
|
1217
|
+
var ODORO_MAIL_PATH = "/api/hosting/site-login-link";
|
|
1218
|
+
function odoroMail(options) {
|
|
1219
|
+
const send = options.fetch ?? fetch;
|
|
1220
|
+
const now = options.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
1221
|
+
const address2 = new URL(ODORO_MAIL_PATH, options.origin).toString();
|
|
1222
|
+
return {
|
|
1223
|
+
sendLoginLink: async ({ email, token }) => {
|
|
1224
|
+
if (options.secret === "") throw new Error("site secret missing");
|
|
1225
|
+
const body = JSON.stringify({ site: options.site, courriel: email, jeton: token });
|
|
1226
|
+
const timestamp = now();
|
|
1227
|
+
const reply = await send(address2, {
|
|
1228
|
+
method: "POST",
|
|
1229
|
+
headers: {
|
|
1230
|
+
"content-type": "application/json",
|
|
1231
|
+
"x-odoro-timestamp": String(timestamp),
|
|
1232
|
+
"x-odoro-signature": signRequest(options.secret, timestamp, body)
|
|
1233
|
+
},
|
|
1234
|
+
body,
|
|
1235
|
+
signal: AbortSignal.timeout(15e3)
|
|
1236
|
+
});
|
|
1237
|
+
if (!reply.ok) throw new Error(`Odoro refused the link (${reply.status})`);
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
|
|
981
1242
|
// src/pg.ts
|
|
982
1243
|
function reader(run) {
|
|
983
1244
|
return {
|
|
@@ -1007,6 +1268,6 @@ function baseFromPool(pool) {
|
|
|
1007
1268
|
};
|
|
1008
1269
|
}
|
|
1009
1270
|
|
|
1010
|
-
export { CART_COOKIE, ODORO_PAYMENT_PATH, SHOP_MAJOR, baseFromPool, codesInstalled, createCommerceModule, discountFor, odoroPayment, productImage, shopInstalled, signCallback, signRequest, storageInstalled, verifyCallback };
|
|
1271
|
+
export { CART_COOKIE, CUSTOMER_COOKIE, LINKS_PER_HOUR, ODORO_MAIL_PATH, ODORO_PAYMENT_PATH, SESSION_MAX_AGE, SHOP_MAJOR, accountsInstalled, baseFromPool, codesInstalled, createCommerceModule, discountFor, odoroMail, odoroPayment, openSession, productImage, shopInstalled, signCallback, signRequest, storageInstalled, verifyCallback };
|
|
1011
1272
|
//# sourceMappingURL=index.js.map
|
|
1012
1273
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/base.ts","../src/images.ts","../src/catalogue.ts","../src/cart.ts","../src/checkout.ts","../src/module.ts","../src/odoro-payment.ts","../src/pg.ts"],"names":["text","count","createHmac"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * What the commerce module needs from a database — and nothing more.\n *\n * `query` and `transaction`, the shape `@odoro-cli/cloud-connect` already has:\n * the app wires its connection, and the tests a plain `pg` pool. The module\n * never opens a connection itself, and never reads a connection string: that\n * belongs to the app's configuration.\n *\n * @module\n */\n\n/** Something that runs SQL: a connection, or a transaction. */\nexport interface Query {\n query<Row extends object = Record<string, unknown>>(\n text: string,\n values?: readonly unknown[],\n ): Promise<{ readonly rows: readonly Row[] }>\n}\n\n/** A database the module can also run a transaction on. */\nexport interface Base extends Query {\n transaction<T>(work: (tx: Query) => Promise<T>): Promise<T>\n}\n\n/** The capability version this module speaks. A `shop` 2.x would not be read. */\nexport const SHOP_MAJOR = 1\n\n/**\n * Is the `shop` capability installed, at a version this module reads?\n *\n * Read in the database's own registry (`odoro.features`), which the capability\n * manager writes. Checked once per process: a capability is not uninstalled\n * under a running site.\n */\nexport async function shopInstalled(base: Query): Promise<boolean> {\n try {\n const { rows } = await base.query<{ version: string }>(\n `SELECT version FROM odoro.features WHERE name = 'shop' AND active`,\n )\n const version = rows[0]?.version\n return (\n version !== undefined &&\n Number.parseInt(version.split('.')[0] ?? '', 10) === SHOP_MAJOR\n )\n } catch {\n return false\n }\n}\n","/**\n * Product images, read from the site's own storage (capability `storage`,\n * DECISIONS §44 of the Odoro repository).\n *\n * `shop.product_images.storage_key` names an object of `storage.objects`. The\n * storefront gives an image two ways, as Odoro's central storefront does:\n * inline in `visuel` (base64, so a card renders without a second request),\n * and as bytes for `geste=image` (so a page points at a URL that follows the\n * merchant's latest photo without republishing).\n *\n * A site whose database has no `storage` capability yet keeps working: its\n * images are `null`, as before. That is checked once per process — a\n * capability is not uninstalled under a running site.\n *\n * @module\n */\n\nimport type { Query } from './base.js'\n\n/** SVG can carry script: it is never served, whatever the database says. */\nconst SERVED_TYPES = new Set([\n 'image/png',\n 'image/jpeg',\n 'image/webp',\n 'image/avif',\n 'image/gif',\n])\n\nexport async function storageInstalled(db: Query): Promise<boolean> {\n try {\n const { rows } = await db.query<{ present: boolean }>(\n `SELECT to_regclass('storage.objects') IS NOT NULL AS present`,\n )\n return rows[0]?.present === true\n } catch {\n return false\n }\n}\n\nexport interface StoredImage {\n readonly bytes: Buffer\n readonly type: string\n}\n\n/**\n * The first image of a product FOR SALE, as bytes. `null` for a draft, an\n * unknown id, a product without image, or a type that is never served.\n */\nexport async function productImage(\n db: Query,\n productId: string,\n): Promise<StoredImage | null> {\n if (!/^[0-9a-f-]{36}$/i.test(productId)) return null\n if (!(await storageInstalled(db))) return null\n const { rows } = await db.query<{ bytes: Buffer; mime_type: string }>(\n `SELECT o.bytes, o.mime_type\n FROM shop.product_images i\n JOIN shop.products p ON p.id = i.product_id\n JOIN storage.objects o ON o.key = i.storage_key\n WHERE p.id = $1 AND p.published AND p.deleted_at IS NULL\n ORDER BY i.position, i.id\n LIMIT 1`,\n [productId],\n )\n const row = rows[0]\n if (row === undefined || !SERVED_TYPES.has(row.mime_type)) return null\n return { bytes: row.bytes, type: row.mime_type }\n}\n\n/** The SQL that yields a product's images as base64, when storage exists. */\nexport const SQL_IMAGES = `\n SELECT encode(o.bytes, 'base64') AS contenu, o.mime_type AS type, i.alt_text AS texte\n FROM shop.product_images i\n JOIN storage.objects o ON o.key = i.storage_key\n WHERE i.product_id = p.id AND o.mime_type <> 'image/svg+xml'\n ORDER BY i.position, i.id`\n","/**\n * The catalogue, read from the `shop` schema in the storefront's shapes.\n *\n * Only what is FOR SALE: published, not deleted. A draft is the merchant's\n * business; a visitor who guesses its id gets the same 404 as for a product\n * that never existed.\n *\n * Availability is per VARIANT — the stock lives there — minus the open,\n * unexpired reservations of carts being paid. An expired reservation holds\n * nothing: an abandoned checkout must not take an item off sale forever.\n *\n * Images: `shop.product_images` names objects of the site's `storage`\n * capability (see `images.ts`). They come inline, base64, as in Odoro's\n * central storefront. A database without `storage` keeps `visuel: null`.\n *\n * @module\n */\n\nimport type {\n CatalogueQuery,\n CollectionEnVitrine,\n FicheProduit,\n ProduitEnVitrine,\n VarianteEnVitrine,\n} from '@odoro-cli/commerce'\n\nimport type { Query } from './base.js'\nimport { SQL_IMAGES, storageInstalled } from './images.js'\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/** A variant is available when its stock, minus what is being paid, covers one. */\nexport const SQL_AVAILABLE = `\n (v.stock IS NULL OR v.stock - coalesce((\n SELECT sum(r.quantity)::integer FROM shop.stock_reservations r\n WHERE r.variant_id = v.id AND r.state = 'ouverte' AND r.expires_at > now()\n ), 0) >= 1)`\n\n/** The sort orders a visitor may ask for: a closed list, never a concatenated `ORDER BY`. */\nconst ORDERS: Readonly<Record<NonNullable<CatalogueQuery['tri']>, string>> = {\n nouveautes: 'p.created_at DESC, p.name',\n prix_croissant: 'p.price_cents ASC, p.name',\n prix_decroissant: 'p.price_cents DESC, p.name',\n nom: 'p.name ASC',\n}\n\ninterface ProductRow {\n id: string\n name: string\n description: string\n kind: string\n price_cents: number\n compare_at_cents: number | null\n available: boolean\n /** The first image, when the database has `storage`. */\n visuel?: { contenu: string; type: string; texte: string } | null\n}\n\n/** The first image of each product, as a SELECT column — or a plain null. */\nfunction colonneDuVisuel(avecStockage: boolean): string {\n return avecStockage\n ? `(SELECT json_build_object('contenu', im.contenu, 'type', im.type, 'texte', im.texte)\n FROM (${SQL_IMAGES} LIMIT 1) im) AS visuel`\n : 'NULL::json AS visuel'\n}\n\nfunction toProduct(r: ProductRow): ProduitEnVitrine {\n return {\n id: r.id,\n nom: r.name,\n description: r.description,\n genre: r.kind,\n prixCentimes: Number(r.price_cents),\n prixBarreCentimes: r.compare_at_cents === null ? null : Number(r.compare_at_cents),\n disponible: r.available === true,\n visuel: r.visuel ?? null,\n etiquettes: [],\n prixUnitaire: null,\n }\n}\n\nexport async function readCatalogue(\n db: Query,\n q: CatalogueQuery = {},\n): Promise<{ produits: ProduitEnVitrine[]; total: number }> {\n const text = (q.q ?? '').trim()\n const values: unknown[] = [\n text === '' ? null : `%${text.toLowerCase()}%`,\n q.collection !== undefined && UUID.test(q.collection) ? q.collection : null,\n q.genre ?? null,\n q.prixMin ?? null,\n q.prixMax ?? null,\n q.disponibles === true,\n ]\n const where = `\n FROM shop.products p\n WHERE p.published AND p.deleted_at IS NULL\n AND ($1::text IS NULL OR lower(p.name) LIKE $1 OR lower(p.description) LIKE $1)\n AND ($2::uuid IS NULL OR EXISTS (\n SELECT 1 FROM shop.collection_products cp\n JOIN shop.collections c ON c.id = cp.collection_id AND c.published\n WHERE cp.collection_id = $2 AND cp.product_id = p.id))\n AND ($3::text IS NULL OR p.kind = $3)\n AND ($4::integer IS NULL OR p.price_cents >= $4)\n AND ($5::integer IS NULL OR p.price_cents <= $5)\n AND (NOT $6::boolean OR EXISTS (\n SELECT 1 FROM shop.product_variants v WHERE v.product_id = p.id AND ${SQL_AVAILABLE}))`\n // An unknown order falls back to the merchant's, silently: it is a sort, not an error.\n const order = (q.tri === undefined ? undefined : ORDERS[q.tri]) ?? 'p.position, p.name'\n const limit = Math.min(Math.max(q.combien ?? 24, 1), 60)\n const offset = Math.max(q.depuis ?? 0, 0)\n\n const avecStockage = await storageInstalled(db)\n const { rows } = await db.query<ProductRow>(\n `SELECT p.id, p.name, p.description, p.kind, p.price_cents, p.compare_at_cents,\n EXISTS (SELECT 1 FROM shop.product_variants v WHERE v.product_id = p.id AND ${SQL_AVAILABLE}) AS available,\n ${colonneDuVisuel(avecStockage)}\n ${where}\n ORDER BY ${order}\n LIMIT $7 OFFSET $8`,\n [...values, limit, offset],\n )\n const { rows: count } = await db.query<{ n: number }>(\n `SELECT count(*)::integer AS n ${where}`,\n values,\n )\n return { produits: rows.map(toProduct), total: Number(count[0]?.n ?? 0) }\n}\n\nexport async function readProduct(db: Query, id: string): Promise<FicheProduit | null> {\n if (!UUID.test(id)) return null\n const avecStockage = await storageInstalled(db)\n const { rows } = await db.query<ProductRow>(\n `SELECT p.id, p.name, p.description, p.kind, p.price_cents, p.compare_at_cents,\n EXISTS (SELECT 1 FROM shop.product_variants v WHERE v.product_id = p.id AND ${SQL_AVAILABLE}) AS available,\n ${colonneDuVisuel(avecStockage)}\n FROM shop.products p\n WHERE p.id = $1 AND p.published AND p.deleted_at IS NULL`,\n [id],\n )\n const row = rows[0]\n if (row === undefined) return null\n\n const { rows: options } = await db.query<{\n id: string\n name: string\n values: string[] | null\n }>(\n `SELECT o.id, o.name,\n array_agg(DISTINCT vv.value) FILTER (WHERE vv.value IS NOT NULL) AS values\n FROM shop.product_options o\n LEFT JOIN shop.product_variant_values vv ON vv.option_id = o.id\n WHERE o.product_id = $1\n GROUP BY o.id, o.name, o.position\n ORDER BY o.position, o.name`,\n [id],\n )\n const { rows: variants } = await db.query<{\n id: string\n price_cents: number\n available: boolean\n choices: { option: string; valeur: string }[] | null\n }>(\n `SELECT v.id,\n coalesce(v.price_cents, p.price_cents) AS price_cents,\n ${SQL_AVAILABLE} AS available,\n (SELECT json_agg(json_build_object('option', o.name, 'valeur', vv.value) ORDER BY o.position, o.name)\n FROM shop.product_variant_values vv\n JOIN shop.product_options o ON o.id = vv.option_id\n WHERE vv.variant_id = v.id) AS choices\n FROM shop.product_variants v\n JOIN shop.products p ON p.id = v.product_id\n WHERE v.product_id = $1\n ORDER BY v.position, v.created_at`,\n [id],\n )\n // Every image of the product, in order, for the product page.\n const visuels = avecStockage\n ? (\n await db.query<{ contenu: string; type: string; texte: string }>(\n `SELECT im.contenu, im.type, im.texte FROM (${SQL_IMAGES}) im`.replace(\n 'p.id',\n '$1',\n ),\n [id],\n )\n ).rows.map((v) => ({ contenu: v.contenu, type: v.type, texte: v.texte }))\n : []\n const product = toProduct(row)\n const variantes: VarianteEnVitrine[] = variants.map((v) => {\n const choix = v.choices ?? []\n const price = Number(v.price_cents)\n return {\n id: v.id,\n libelle:\n choix.length === 0\n ? row.name\n : choix.map((c) => `${c.option} : ${c.valeur}`).join(' / '),\n choix,\n prixCentimes: price,\n prixBarreCentimes:\n row.compare_at_cents !== null && row.compare_at_cents > price\n ? Number(row.compare_at_cents)\n : null,\n disponible: v.available === true,\n visuelRang: null,\n prixUnitaire: null,\n }\n })\n return {\n ...product,\n seoTitre: row.name,\n seoResume: row.description,\n visuels,\n options: options.map((o) => ({ id: o.id, nom: o.name, valeurs: o.values ?? [] })),\n variantes,\n }\n}\n\nexport async function readCollections(db: Query): Promise<CollectionEnVitrine[]> {\n const { rows } = await db.query<{\n id: string\n name: string\n description: string\n n: number\n }>(\n `SELECT c.id, c.name, c.description,\n (SELECT count(*)::integer FROM shop.collection_products cp\n JOIN shop.products p ON p.id = cp.product_id\n WHERE cp.collection_id = c.id AND p.published AND p.deleted_at IS NULL) AS n\n FROM shop.collections c\n WHERE c.published\n ORDER BY c.name`,\n )\n return rows.map((c) => ({\n id: c.id,\n nom: c.name,\n description: c.description,\n combien: Number(c.n),\n }))\n}\n","/**\n * The cart, found by its token.\n *\n * The token travels in an `HttpOnly` cookie: no script of the page reads it,\n * so no third-party script injected into a site can copy it elsewhere. It is\n * set on a GESTURE (an add), never on a visit — a site that sets a cookie on\n * every page view would need a consent banner for it.\n *\n * The unit price is recorded at the add: checkout compares it with the price of\n * the day, and says so when it changed.\n *\n * @module\n */\n\nimport { randomBytes } from 'node:crypto'\n\nimport type { LigneDePanier, PanierEnVitrine } from '@odoro-cli/commerce'\n\nimport type { Query } from './base.js'\nimport { SQL_AVAILABLE } from './catalogue.js'\n\nexport const CART_COOKIE = 'odoro_panier'\n/** Thirty days: the window of an abandoned-cart reminder, no longer. */\nexport const CART_MAX_AGE = 60 * 60 * 24 * 30\n\nconst TOKEN = /^vitrine-[0-9a-f]{48}$/\n\nexport function isCartToken(value: string | undefined): value is string {\n return value !== undefined && TOKEN.test(value)\n}\n\nexport function newCartToken(): string {\n return `vitrine-${randomBytes(24).toString('hex')}`\n}\n\n/** Refused gestures carry the sentence the visitor reads. */\nexport class CartError extends Error {}\n\nexport async function findCart(\n db: Query,\n token: string | undefined,\n): Promise<{ id: string; token: string } | null> {\n if (!isCartToken(token)) return null\n const { rows } = await db.query<{ id: string }>(\n `SELECT id FROM shop.carts WHERE token = $1 AND state = 'ouvert' AND NOT preview`,\n [token],\n )\n const row = rows[0]\n return row === undefined ? null : { id: row.id, token }\n}\n\nexport async function openCart(db: Query): Promise<{ id: string; token: string }> {\n const token = newCartToken()\n const { rows } = await db.query<{ id: string }>(\n `INSERT INTO shop.carts (token) VALUES ($1) RETURNING id`,\n [token],\n )\n return { id: rows[0]!.id, token }\n}\n\nasync function touch(db: Query, cartId: string): Promise<void> {\n await db.query('UPDATE shop.carts SET updated_at = now() WHERE id = $1', [cartId])\n}\n\n/**\n * Adds a variant. The join on a PUBLISHED product is the guard: a guessed\n * variant id of a draft adds nothing.\n */\nexport async function addToCart(\n db: Query,\n cartId: string,\n variantId: string,\n quantity: number,\n): Promise<void> {\n const qty = Math.min(Math.max(Math.trunc(Number(quantity) || 1), 1), 99)\n const { rows } = await db.query<{ variant_id: string }>(\n `INSERT INTO shop.cart_lines (cart_id, variant_id, quantity, unit_price_cents)\n SELECT $1, v.id, $3, coalesce(v.price_cents, p.price_cents)\n FROM shop.product_variants v\n JOIN shop.products p ON p.id = v.product_id\n WHERE v.id = $2 AND p.published AND p.deleted_at IS NULL\n ON CONFLICT (cart_id, variant_id)\n DO UPDATE SET quantity = least(shop.cart_lines.quantity + excluded.quantity, 99)\n RETURNING variant_id`,\n [cartId, variantId, qty],\n )\n if (rows.length === 0) throw new CartError(\"Cet article n'est plus en vente.\")\n await touch(db, cartId)\n}\n\n/** A quantity. Zero removes the line — the gesture the screen expects. */\nexport async function setQuantity(\n db: Query,\n cartId: string,\n variantId: string,\n quantity: number,\n): Promise<void> {\n const qty = Math.min(Math.max(Math.trunc(Number(quantity) || 0), 0), 99)\n if (qty === 0) {\n await db.query('DELETE FROM shop.cart_lines WHERE cart_id = $1 AND variant_id = $2', [\n cartId,\n variantId,\n ])\n } else {\n await db.query(\n 'UPDATE shop.cart_lines SET quantity = $3 WHERE cart_id = $1 AND variant_id = $2',\n [cartId, variantId, qty],\n )\n }\n await touch(db, cartId)\n}\n\nexport async function readCart(\n db: Query,\n cart: { id: string; token: string },\n isNew = false,\n): Promise<PanierEnVitrine> {\n const { rows } = await db.query<{\n variant_id: string\n product_id: string\n name: string\n label: string | null\n unit_price_cents: number\n quantity: number\n available: boolean\n }>(\n `SELECT l.variant_id, v.product_id, p.name,\n (SELECT string_agg(vv.value, ' / ' ORDER BY o.position)\n FROM shop.product_variant_values vv\n JOIN shop.product_options o ON o.id = vv.option_id\n WHERE vv.variant_id = v.id) AS label,\n l.unit_price_cents, l.quantity,\n (p.published AND p.deleted_at IS NULL AND ${SQL_AVAILABLE}) AS available\n FROM shop.cart_lines l\n JOIN shop.product_variants v ON v.id = l.variant_id\n JOIN shop.products p ON p.id = v.product_id\n WHERE l.cart_id = $1\n ORDER BY l.added_at, l.variant_id`,\n [cart.id],\n )\n const { rows: head } = await db.query<{ email: string | null }>(\n 'SELECT email FROM shop.carts WHERE id = $1',\n [cart.id],\n )\n const lignes: LigneDePanier[] = rows.map((l) => ({\n varianteId: l.variant_id,\n produitId: l.product_id,\n nom: l.name,\n declinaison: l.label ?? '',\n prixUnitaireCentimes: Number(l.unit_price_cents),\n quantite: Number(l.quantity),\n sousTotalCentimes: Number(l.unit_price_cents) * Number(l.quantity),\n disponible: l.available === true,\n visuel: null,\n }))\n return {\n panierId: cart.id,\n jeton: cart.token,\n courriel: head[0]?.email ?? null,\n lignes,\n combien: lignes.reduce((n, l) => n + l.quantite, 0),\n sousTotalCentimes: lignes.reduce((n, l) => n + l.sousTotalCentimes, 0),\n neuf: isNew,\n }\n}\n","/**\n * Checkout: the order lives in the site's database, the MONEY does not.\n *\n * Decided with the founder of Odoro (DECISIONS §43 of the main repository):\n * the payment stays with Odoro (Whop), which knows money and its obligations.\n * This module opens the order, reserves the stock, and hands the payment to a\n * PORT the app wires — Odoro's payment API. The order only keeps the opaque\n * reference the payment returned; it learns the outcome through a SIGNED\n * callback (`confirmPayment`), never from the visitor's browser.\n *\n * The three gestures of the storefront contract:\n *\n * · `ouvrir` — reprices the cart at today's prices (and says what changed),\n * checks and reserves the stock for thirty minutes, writes the order;\n * · `chiffrer` — the total, with the discount code the visitor typed. The\n * code is priced HERE, from the shop's own `discount_codes` (shop 1.2.0,\n * copied from Odoro): the browser only names it. An unknown code is\n * refused by name, never silently ignored;\n * · `payer` — the payment page, from the port.\n *\n * @module\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto'\n\nimport type { Base, Query } from './base.js'\nimport { CartError } from './cart.js'\n\n/** A refused checkout gesture: `status` and the sentence the visitor reads. */\nexport class CheckoutError extends Error {\n constructor(\n readonly status: 400 | 404 | 409,\n message: string,\n ) {\n super(message)\n }\n}\n\n/** What the visitor typed, under the storefront contract's names. */\nexport interface CheckoutInput {\n courriel: string\n nom: string\n ligne1: string\n ligne2?: string\n code_postal: string\n ville: string\n pays: string\n telephone?: string\n}\n\n/** Where the money goes: Odoro's payment, wired by the app. */\nexport interface PaymentPort {\n open(input: {\n readonly orderId: string\n readonly number: number\n readonly totalCents: number\n readonly currency: string\n readonly email: string\n }): Promise<{ readonly reference: string; readonly paymentUrl: string }>\n}\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n/** How long a checkout holds the stock. */\nconst RESERVATION_MINUTES = 30\n\nfunction need(value: string | undefined, message: string): string {\n const v = (value ?? '').trim()\n if (v === '') throw new CheckoutError(400, message)\n return v\n}\n\ninterface OrderRow {\n id: string\n number: string | number\n state: string\n email: string\n currency: string\n subtotal_cents: number\n shipping_cents: number\n tax_cents: number\n total_cents: number\n /** Absent on a shop still in 1.1.0 — see `codesInstalled`. */\n discount_code?: string | null\n discount_cents?: number\n payment_ref: string | null\n}\n\n/**\n * Are the discount codes installed (shop 1.2.0)? A shop still in 1.1.0 keeps\n * selling: a code is refused by name there, as before this module served\n * them, and nothing reads a column the shop does not have.\n */\nexport async function codesInstalled(db: Query): Promise<boolean> {\n try {\n const { rows } = await db.query<{ present: boolean }>(\n `SELECT to_regclass('shop.discount_codes') IS NOT NULL AS present`,\n )\n return rows[0]?.present === true\n } catch {\n return false\n }\n}\n\nexport async function quote(db: Query, orderId: string) {\n const { rows } = await db.query<OrderRow>('SELECT * FROM shop.orders WHERE id = $1', [\n orderId,\n ])\n const order = rows[0]\n if (order === undefined) throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n const { rows: lines } = await db.query<{\n variant_id: string | null\n product_id: string | null\n product_name: string\n unit_price_cents: number\n quantity: number\n }>(\n `SELECT l.variant_id, v.product_id, l.product_name, l.unit_price_cents, l.quantity\n FROM shop.order_lines l LEFT JOIN shop.product_variants v ON v.id = l.variant_id\n WHERE l.order_id = $1 ORDER BY l.product_name`,\n [orderId],\n )\n return {\n caisseId: order.id,\n devise: order.currency,\n lignes: lines.map((l) => ({\n varianteId: l.variant_id ?? '',\n produitId: l.product_id ?? '',\n nom: l.product_name,\n genre: 'physique',\n prixUnitaireCentimes: Number(l.unit_price_cents),\n quantite: Number(l.quantity),\n sousTotalCentimes: Number(l.unit_price_cents) * Number(l.quantity),\n taxable: true,\n })),\n remises:\n (order.discount_code ?? null) === null\n ? []\n : [\n {\n remiseId: null,\n code: order.discount_code ?? '',\n libelle: order.discount_code ?? '',\n montantCentimes: Number(order.discount_cents ?? 0),\n },\n ],\n carteCadeau: null,\n methodeDeLivraison: null,\n sousTotalCentimes: Number(order.subtotal_cents),\n remiseCentimes: Number(order.discount_cents ?? 0),\n portCentimes: Number(order.shipping_cents),\n taxeCentimes: Number(order.tax_cents),\n carteCadeauCentimes: 0,\n totalCentimes: Number(order.total_cents),\n taxeIncluse: true,\n }\n}\n\nexport async function openCheckout(\n db: Base,\n cartId: string,\n input: CheckoutInput,\n currency = 'EUR',\n): Promise<{\n orderId: string\n repriced: { varianteId: string; ancienCentimes: number; nouveauCentimes: number }[]\n}> {\n const email = need(input.courriel, 'Indiquez votre adresse e-mail.')\n if (!EMAIL.test(email))\n throw new CheckoutError(400, \"Cette adresse e-mail n'est pas valable.\")\n const name = need(input.nom, 'Indiquez votre nom.')\n const address = {\n ligne1: need(input.ligne1, 'Indiquez votre adresse.'),\n ligne2: (input.ligne2 ?? '').trim(),\n code_postal: need(input.code_postal, 'Indiquez votre code postal.'),\n ville: need(input.ville, 'Indiquez votre ville.'),\n pays: need(input.pays, 'Indiquez votre pays.'),\n telephone: (input.telephone ?? '').trim(),\n }\n\n return await db.transaction(async (tx) => {\n // A checkout reopened on the same cart replaces the previous one: its\n // reservation is released, and only one open order stays.\n await tx.query(\n `UPDATE shop.stock_reservations SET state = 'relachee'\n WHERE cart_id = $1 AND state = 'ouverte'`,\n [cartId],\n )\n await tx.query(\n `UPDATE shop.orders SET state = 'annulee'\n WHERE cart_id = $1 AND state = 'ouverte' AND payment_ref IS NULL`,\n [cartId],\n )\n\n const { rows: lines } = await tx.query<{\n variant_id: string\n quantity: number\n unit_price_cents: number\n current_cents: number\n stock: number | null\n for_sale: boolean\n name: string\n label: string | null\n }>(\n `SELECT l.variant_id, l.quantity, l.unit_price_cents,\n coalesce(v.price_cents, p.price_cents) AS current_cents,\n v.stock, (p.published AND p.deleted_at IS NULL) AS for_sale, p.name,\n (SELECT string_agg(vv.value, ' / ' ORDER BY o.position)\n FROM shop.product_variant_values vv JOIN shop.product_options o ON o.id = vv.option_id\n WHERE vv.variant_id = v.id) AS label\n FROM shop.cart_lines l\n JOIN shop.product_variants v ON v.id = l.variant_id\n JOIN shop.products p ON p.id = v.product_id\n WHERE l.cart_id = $1\n ORDER BY l.variant_id\n FOR UPDATE OF v`,\n [cartId],\n )\n if (lines.length === 0) throw new CartError('Votre panier est vide.')\n const gone = lines.find((l) => !l.for_sale)\n if (gone !== undefined) {\n throw new CheckoutError(\n 409,\n `« ${gone.name} » n'est plus en vente : retirez-le de votre panier.`,\n )\n }\n\n for (const l of lines) {\n if (l.stock === null) continue\n const { rows } = await tx.query<{ held: number }>(\n `SELECT coalesce(sum(quantity), 0)::integer AS held FROM shop.stock_reservations\n WHERE variant_id = $1 AND state = 'ouverte' AND expires_at > now()`,\n [l.variant_id],\n )\n if (Number(l.stock) - Number(rows[0]?.held ?? 0) < Number(l.quantity)) {\n throw new CheckoutError(409, `« ${l.name} » : il n'en reste pas assez.`)\n }\n }\n\n const repriced = lines\n .filter((l) => Number(l.current_cents) !== Number(l.unit_price_cents))\n .map((l) => ({\n varianteId: l.variant_id,\n ancienCentimes: Number(l.unit_price_cents),\n nouveauCentimes: Number(l.current_cents),\n }))\n for (const r of repriced) {\n await tx.query(\n 'UPDATE shop.cart_lines SET unit_price_cents = $3 WHERE cart_id = $1 AND variant_id = $2',\n [cartId, r.varianteId, r.nouveauCentimes],\n )\n }\n\n const subtotal = lines.reduce(\n (n, l) => n + Number(l.current_cents) * Number(l.quantity),\n 0,\n )\n const { rows: created } = await tx.query<{ id: string }>(\n `INSERT INTO shop.orders (cart_id, email, customer_name, shipping_address, currency, subtotal_cents, total_cents)\n VALUES ($1, $2, $3, $4, $5, $6, $6) RETURNING id`,\n [cartId, email, name, JSON.stringify(address), currency, subtotal],\n )\n const orderId = created[0]!.id\n for (const l of lines) {\n await tx.query(\n `INSERT INTO shop.order_lines (order_id, variant_id, product_name, variant_label, unit_price_cents, quantity)\n VALUES ($1, $2, $3, $4, $5, $6)`,\n [orderId, l.variant_id, l.name, l.label ?? '', l.current_cents, l.quantity],\n )\n if (l.stock !== null) {\n await tx.query(\n `INSERT INTO shop.stock_reservations (variant_id, cart_id, quantity, expires_at)\n VALUES ($1, $2, $3, now() + make_interval(mins => $4))`,\n [l.variant_id, cartId, l.quantity, RESERVATION_MINUTES],\n )\n }\n }\n await tx.query('UPDATE shop.carts SET email = $2, updated_at = now() WHERE id = $1', [\n cartId,\n email,\n ])\n return { orderId, repriced }\n })\n}\n\n/**\n * What a code takes off a subtotal. Rounded DOWN: a rounding never gives the\n * buyer a cent the seller did not offer. Never more than the subtotal.\n *\n * A percentage is counted in thousandths of a percent (10000 = 10 %), as Odoro\n * counts it — the codes are copied from there unchanged.\n */\nexport function discountFor(\n kind: 'pourcentage' | 'montant',\n value: number,\n subtotalCents: number,\n): number {\n const raw =\n kind === 'pourcentage' ? Math.floor((subtotalCents * value) / 100_000) : value\n return Math.max(0, Math.min(raw, subtotalCents))\n}\n\nfunction euros(cents: number, currency: string): string {\n return new Intl.NumberFormat('fr', { style: 'currency', currency }).format(cents / 100)\n}\n\n/**\n * The quote with the code the visitor typed — or without, when the field is\n * empty: every `chiffrer` and every `payer` names the code, and the order\n * carries exactly that one.\n *\n * Once the payment is open, the total is frozen: Odoro refuses a payment\n * whose amount changed under the same order. Naming the same code again is\n * fine; another one asks to reopen the checkout.\n */\nexport async function applyCode(db: Query, orderId: string, code: string | undefined) {\n const wanted = (code ?? '').trim()\n if (!(await codesInstalled(db))) {\n if (wanted !== '')\n throw new CheckoutError(409, \"Ce code n'est pas reconnu par cette boutique.\")\n return await quote(db, orderId)\n }\n const { rows } = await db.query<OrderRow>('SELECT * FROM shop.orders WHERE id = $1', [\n orderId,\n ])\n const order = rows[0]\n if (order === undefined) throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n if (order.state !== 'ouverte' || order.payment_ref !== null) {\n if (wanted.toUpperCase() === (order.discount_code ?? '').toUpperCase())\n return await quote(db, orderId)\n throw new CheckoutError(\n 409,\n 'Le paiement de cette commande est déjà ouvert : rouvrez la caisse pour changer de code.',\n )\n }\n\n const subtotal = Number(order.subtotal_cents)\n let discount = 0\n let applied: string | null = null\n if (wanted !== '') {\n const { rows: found } = await db.query<{\n id: string\n code: string\n kind: 'pourcentage' | 'montant'\n value: number\n min_subtotal_cents: number\n max_uses: number | null\n used: number\n }>(\n `SELECT c.id, c.code, c.kind, c.value, c.min_subtotal_cents, c.max_uses,\n (SELECT count(*) FROM shop.discount_uses u WHERE u.discount_id = c.id)::integer AS used\n FROM shop.discount_codes c\n WHERE upper(c.code) = upper($1) AND c.active\n AND (c.starts_at IS NULL OR c.starts_at <= now())\n AND (c.ends_at IS NULL OR c.ends_at > now())`,\n [wanted],\n )\n const found0 = found[0]\n if (found0 === undefined)\n throw new CheckoutError(409, \"Ce code n'est pas reconnu par cette boutique.\")\n if (found0.max_uses !== null && Number(found0.used) >= Number(found0.max_uses))\n throw new CheckoutError(409, 'Ce code a déjà servi autant de fois que prévu.')\n if (subtotal < Number(found0.min_subtotal_cents))\n throw new CheckoutError(\n 409,\n `Ce code demande un panier d'au moins ${euros(Number(found0.min_subtotal_cents), order.currency)}.`,\n )\n discount = discountFor(found0.kind, Number(found0.value), subtotal)\n // Nothing is free here: a payment of zero cannot be opened, and a code\n // that erased the whole order would leave the visitor at a dead end.\n if (subtotal - discount + Number(order.shipping_cents) + Number(order.tax_cents) <= 0)\n throw new CheckoutError(409, 'Ce code ne peut pas rendre la commande gratuite.')\n applied = found0.code\n }\n\n await db.query(\n `UPDATE shop.orders\n SET discount_code = $2, discount_cents = $3,\n total_cents = subtotal_cents - $3 + shipping_cents + tax_cents\n WHERE id = $1 AND state = 'ouverte' AND payment_ref IS NULL`,\n [orderId, applied, discount],\n )\n return await quote(db, orderId)\n}\n\nexport async function pay(db: Query, orderId: string, port: PaymentPort) {\n const { rows } = await db.query<OrderRow & { expired: boolean }>(\n `SELECT o.*,\n EXISTS (SELECT 1 FROM shop.stock_reservations r\n WHERE r.cart_id = o.cart_id AND r.state = 'ouverte' AND r.expires_at <= now()) AS expired\n FROM shop.orders o WHERE o.id = $1`,\n [orderId],\n )\n const order = rows[0]\n if (order === undefined) throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n if (order.state !== 'ouverte')\n throw new CheckoutError(409, 'Cette commande ne peut plus être payée.')\n if (order.expired)\n throw new CheckoutError(409, 'Votre réservation a expiré : rouvrez la caisse.')\n\n const opened = await port.open({\n orderId: order.id,\n number: Number(order.number),\n totalCents: Number(order.total_cents),\n currency: order.currency,\n email: order.email,\n })\n await db.query('UPDATE shop.orders SET payment_ref = $2 WHERE id = $1', [\n order.id,\n opened.reference,\n ])\n const total = Number(order.total_cents)\n return {\n commandeId: order.id,\n numero: Number(order.number),\n jetonDeSuivi: order.id,\n totalCentimes: total,\n totalTexte: new Intl.NumberFormat('fr', {\n style: 'currency',\n currency: order.currency,\n }).format(total / 100),\n adresseDePaiement: opened.paymentUrl,\n }\n}\n\n/** How long a signed callback stays valid. */\nconst CALLBACK_WINDOW_S = 300\n\n/** The signature of a payment callback: HMAC-SHA256 of `reference.state.timestamp`. */\nexport function signCallback(\n secret: string,\n reference: string,\n state: string,\n timestamp: number,\n): string {\n return createHmac('sha256', secret)\n .update(`${reference}.${state}.${timestamp}`)\n .digest('hex')\n}\n\nexport function verifyCallback(\n secret: string,\n input: { reference: string; etat: string; horodatage: number; signature: string },\n now = Date.now(),\n): boolean {\n if (secret === '' || Math.abs(now / 1000 - input.horodatage) > CALLBACK_WINDOW_S)\n return false\n const expected = Buffer.from(\n signCallback(secret, input.reference, input.etat, input.horodatage),\n )\n const received = Buffer.from(input.signature)\n return expected.length === received.length && timingSafeEqual(expected, received)\n}\n\n/**\n * The payment's outcome, from Odoro. Idempotent: a callback delivered twice\n * changes nothing the second time.\n */\nexport async function confirmPayment(\n db: Base,\n reference: string,\n state: 'payee' | 'echouee',\n): Promise<boolean> {\n return await db.transaction(async (tx) => {\n const { rows } = await tx.query<{\n id: string\n cart_id: string | null\n state: string\n }>('SELECT id, cart_id, state FROM shop.orders WHERE payment_ref = $1 FOR UPDATE', [\n reference,\n ])\n const order = rows[0]\n if (order === undefined) return false\n if (order.state !== 'ouverte') return true\n\n if (state === 'echouee') {\n await tx.query(`UPDATE shop.orders SET state = 'echouee' WHERE id = $1`, [order.id])\n await tx.query(\n `UPDATE shop.stock_reservations SET state = 'relachee' WHERE cart_id = $1 AND state = 'ouverte'`,\n [order.cart_id],\n )\n return true\n }\n\n await tx.query(\n `UPDATE shop.orders SET state = 'payee', paid_at = now() WHERE id = $1`,\n [order.id],\n )\n // A code counts once it is PAID for: an abandoned checkout uses nothing.\n if (await codesInstalled(tx))\n await tx.query(\n `INSERT INTO shop.discount_uses (order_id, discount_id)\n SELECT o.id, c.id FROM shop.orders o\n JOIN shop.discount_codes c ON upper(c.code) = upper(o.discount_code)\n WHERE o.id = $1\n ON CONFLICT (order_id) DO NOTHING`,\n [order.id],\n )\n // The stock leaves when the money arrives, not before.\n await tx.query(\n `UPDATE shop.product_variants v\n SET stock = greatest(v.stock - l.quantity, 0)\n FROM shop.order_lines l\n WHERE l.order_id = $1 AND l.variant_id = v.id AND v.stock IS NOT NULL`,\n [order.id],\n )\n await tx.query(\n `UPDATE shop.stock_reservations SET state = 'consommee' WHERE cart_id = $1 AND state = 'ouverte'`,\n [order.cart_id],\n )\n await tx.query(\n `UPDATE shop.carts SET state = 'commande', updated_at = now() WHERE id = $1`,\n [order.cart_id],\n )\n return true\n })\n}\n\n/** An order's state, for the tracking link the buyer received. */\nexport async function orderState(db: Query, orderId: string) {\n if (!/^[0-9a-f-]{36}$/i.test(orderId)) return null\n const { rows } = await db.query<OrderRow & { created_at: Date }>(\n 'SELECT * FROM shop.orders WHERE id = $1',\n [orderId],\n )\n const order = rows[0]\n if (order === undefined) return null\n const { rows: lines } = await db.query<{\n product_name: string\n quantity: number\n unit_price_cents: number\n }>(\n 'SELECT product_name, quantity, unit_price_cents FROM shop.order_lines WHERE order_id = $1 ORDER BY product_name',\n [orderId],\n )\n return {\n numero: Number(order.number),\n etat: order.state,\n courriel: order.email,\n totalCentimes: Number(order.total_cents),\n passeeLe: new Date(order.created_at).toISOString(),\n lignes: lines.map((l) => ({\n nom: l.product_name,\n quantite: Number(l.quantity),\n sousTotalCentimes: Number(l.unit_price_cents) * Number(l.quantity),\n })),\n livraisons: [],\n }\n}\n","/**\n * The storefront contract, as an `@odoro-cli/server` module.\n *\n * A site that sells, running in its own container with its own database,\n * mounts this module and serves `/api/storefront/*` itself — the same answers\n * as Odoro's central storefront, so the V4 script (`odoro-boutique.js`) and\n * `@odoro-cli/commerce` talk to it without knowing which one they reach.\n *\n * ## What it serves, and what it does not yet\n *\n * The catalogue, product pages, collections, the cart, checkout (open, quote,\n * pay through Odoro) and order tracking. Customer accounts and discount codes\n * are not served here yet: they answer so, by name — an account request says\n * the account is not available, a code says it is not recognised. A gesture\n * that silently does nothing is worse than one that says no.\n *\n * ## Errors speak the contract\n *\n * The storefront's clients read `erreur`. Every refusal is a problem document\n * (RFC 9457, like the rest of the kernel) that also carries `erreur`.\n *\n * @module\n */\n\nimport {\n ApiError,\n ConflictError,\n NotFoundError,\n ServiceUnavailableError,\n defineModule,\n route,\n type Cookies,\n} from '@odoro-cli/server'\nimport { z } from 'zod'\n\nimport { type Base, shopInstalled } from './base.js'\nimport {\n CART_COOKIE,\n CART_MAX_AGE,\n CartError,\n addToCart,\n findCart,\n openCart,\n readCart,\n setQuantity,\n} from './cart.js'\nimport { readCatalogue, readCollections, readProduct } from './catalogue.js'\nimport {\n CheckoutError,\n applyCode,\n confirmPayment,\n openCheckout,\n orderState,\n pay,\n quote,\n verifyCallback,\n type PaymentPort,\n} from './checkout.js'\n\nexport interface CommerceOptions {\n /** The site's shop database (`@odoro-cli/cloud-connect`, or any `Base`). */\n readonly db: Base\n /** Where the money goes: Odoro's payment. */\n readonly payment: PaymentPort\n /** The secret Odoro signs its payment callbacks with. Empty = callbacks refused. */\n readonly callbackSecret: string\n /** The shop's currency. @defaultValue 'EUR' */\n readonly currency?: string\n /** Mark the cart cookie `Secure`. @defaultValue true */\n readonly secureCookie?: boolean\n}\n\nfunction refusal(\n kind: 'VALIDATION' | 'NOT_FOUND' | 'CONFLICT' | 'UNAVAILABLE',\n message: string,\n): ApiError {\n const extensions = { extensions: { erreur: message } }\n if (kind === 'NOT_FOUND') return new NotFoundError(message, extensions)\n if (kind === 'CONFLICT') return new ConflictError(message, extensions)\n if (kind === 'UNAVAILABLE')\n return new ServiceUnavailableError(message, undefined, extensions)\n return new ApiError('VALIDATION', message, extensions)\n}\n\n/** Domain refusals become contract errors; anything else stays an internal error. */\nasync function speaking<T>(work: () => Promise<T>): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n if (cause instanceof CheckoutError) {\n throw refusal(\n cause.status === 404\n ? 'NOT_FOUND'\n : cause.status === 409\n ? 'CONFLICT'\n : 'VALIDATION',\n cause.message,\n )\n }\n if (cause instanceof CartError) throw refusal('CONFLICT', cause.message)\n throw cause\n }\n}\n\nconst text = z.string().max(200).optional()\nconst count = z.coerce.number().int().min(0).max(100_000).optional()\n\nexport function createCommerceModule(options: CommerceOptions) {\n const { db } = options\n const currency = options.currency ?? 'EUR'\n let installed: Promise<boolean> | null = null\n\n /** Once per process: the `shop` capability, at a version this module reads. */\n async function ready(): Promise<void> {\n installed ??= shopInstalled(db)\n if (!(await installed)) {\n installed = null\n throw refusal('UNAVAILABLE', \"La boutique de ce site n'est pas encore installée.\")\n }\n }\n\n async function cartOf(cookies: Cookies) {\n return await findCart(db, cookies.get(CART_COOKIE))\n }\n\n function keep(cookies: Cookies, token: string): void {\n cookies.set(CART_COOKIE, token, {\n httpOnly: true,\n sameSite: 'lax',\n maxAge: CART_MAX_AGE,\n ...(options.secureCookie === false ? { secure: false } : {}),\n })\n }\n\n const storefront = route({\n name: 'storefront.read',\n method: 'GET',\n path: '/api/storefront',\n auth: 'public',\n input: z.object({\n geste: z\n .enum(['catalogue', 'fiche', 'collections', 'commande', 'suggestions'])\n .default('catalogue'),\n produit: text,\n jeton: text,\n q: text,\n collection: text,\n genre: text,\n prix_min: count,\n prix_max: count,\n disponibles: z.string().optional(),\n tri: z\n .enum(['nouveautes', 'prix_croissant', 'prix_decroissant', 'nom'])\n .optional()\n .catch(undefined),\n combien: count,\n depuis: count,\n }),\n summary:\n 'The catalogue, a product page, the collections or an order, per the storefront contract.',\n handler: async ({ input }) => {\n await ready()\n if (input.geste === 'fiche') {\n const fiche = await readProduct(db, input.produit ?? '')\n if (fiche === null) throw refusal('NOT_FOUND', \"Cet article n'est plus en vente.\")\n return { fiche }\n }\n if (input.geste === 'collections') return { collections: await readCollections(db) }\n if (input.geste === 'commande') {\n const commande = await orderState(db, input.jeton ?? '')\n if (commande === null) throw refusal('NOT_FOUND', \"Cette commande n'existe pas.\")\n return { commande }\n }\n if (input.geste === 'suggestions') {\n const { produits } = await readCatalogue(db, { q: input.q ?? '', combien: 6 })\n return { suggestions: produits.map((p) => ({ id: p.id, nom: p.nom })) }\n }\n return await readCatalogue(db, {\n ...(input.q === undefined ? {} : { q: input.q }),\n ...(input.collection === undefined ? {} : { collection: input.collection }),\n ...(input.genre === undefined ? {} : { genre: input.genre }),\n ...(input.prix_min === undefined ? {} : { prixMin: input.prix_min }),\n ...(input.prix_max === undefined ? {} : { prixMax: input.prix_max }),\n disponibles: input.disponibles === '1',\n ...(input.tri === undefined ? {} : { tri: input.tri }),\n ...(input.combien === undefined ? {} : { combien: input.combien }),\n ...(input.depuis === undefined ? {} : { depuis: input.depuis }),\n })\n },\n })\n\n /** A page view. Nothing is counted or stored here: no tracker on the site. */\n const visit = route({\n name: 'storefront.visit',\n method: 'POST',\n path: '/api/storefront',\n auth: 'public',\n handler: () => ({ ok: true }),\n })\n\n const readCartRoute = route({\n name: 'storefront.cart.read',\n method: 'GET',\n path: '/api/storefront/cart',\n auth: 'public',\n handler: async ({ cookies }) => {\n await ready()\n const cart = await cartOf(cookies)\n if (cart === null) {\n return {\n panier: {\n panierId: '',\n jeton: '',\n courriel: null,\n lignes: [],\n combien: 0,\n sousTotalCentimes: 0,\n neuf: false,\n },\n }\n }\n return { panier: await readCart(db, cart) }\n },\n })\n\n const changeCart = route({\n name: 'storefront.cart.change',\n method: 'POST',\n path: '/api/storefront/cart',\n auth: 'public',\n input: z.object({\n geste: z.enum(['ajouter', 'quantite']).default('ajouter'),\n variante: z.string().regex(/^[0-9a-f-]{36}$/i, \"Cet article n'est plus en vente.\"),\n quantite: z.coerce.number().int().min(0).max(99).default(1),\n }),\n handler: async ({ input, cookies }) =>\n await speaking(async () => {\n await ready()\n const existing = await cartOf(cookies)\n const cart = existing ?? (await openCart(db))\n if (input.geste === 'quantite')\n await setQuantity(db, cart.id, input.variante, input.quantite)\n else await addToCart(db, cart.id, input.variante, input.quantite)\n // The cookie is set on a GESTURE, never on a visit.\n if (existing === null) keep(cookies, cart.token)\n return { panier: await readCart(db, cart, existing === null) }\n }),\n })\n\n const checkout = route({\n name: 'storefront.checkout',\n method: 'POST',\n path: '/api/storefront/checkout',\n auth: 'public',\n input: z.object({\n geste: z.enum(['ouvrir', 'chiffrer', 'payer']),\n caisse: z.string().max(64).optional(),\n code: z.string().max(40).optional(),\n courriel: text,\n nom: text,\n ligne1: text,\n ligne2: text,\n code_postal: z.string().max(20).optional(),\n ville: text,\n pays: z.string().max(40).optional(),\n telephone: z.string().max(30).optional(),\n }),\n handler: async ({ input, cookies }) =>\n await speaking(async () => {\n await ready()\n if (input.geste === 'ouvrir') {\n const cart = await cartOf(cookies)\n if (cart === null) throw new CheckoutError(400, 'Votre panier est vide.')\n const opened = await openCheckout(\n db,\n cart.id,\n {\n courriel: input.courriel ?? '',\n nom: input.nom ?? '',\n ligne1: input.ligne1 ?? '',\n ligne2: input.ligne2 ?? '',\n code_postal: input.code_postal ?? '',\n ville: input.ville ?? '',\n pays: input.pays ?? '',\n telephone: input.telephone ?? '',\n },\n currency,\n )\n return {\n caisse: opened.orderId,\n prixRevalorises: opened.repriced,\n total: await quote(db, opened.orderId),\n }\n }\n const orderId = input.caisse ?? ''\n if (!/^[0-9a-f-]{36}$/i.test(orderId))\n throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n if (input.geste === 'chiffrer')\n return { total: await applyCode(db, orderId, input.code) }\n await applyCode(db, orderId, input.code)\n return await pay(db, orderId, options.payment)\n }),\n })\n\n /** Odoro tells the payment's outcome. Signed: the browser never decides that an order is paid. */\n const callback = route({\n name: 'storefront.payment.callback',\n method: 'POST',\n path: '/api/storefront/payment-callback',\n auth: 'public',\n input: z.object({\n reference: z.string().min(1).max(200),\n etat: z.enum(['payee', 'echouee']),\n horodatage: z.coerce.number().int(),\n signature: z.string().min(1).max(128),\n }),\n handler: async ({ input }) => {\n await ready()\n if (!verifyCallback(options.callbackSecret, input)) {\n throw refusal('NOT_FOUND', 'Rappel de paiement refusé.')\n }\n const known = await confirmPayment(db, input.reference, input.etat)\n if (!known) throw refusal('NOT_FOUND', 'Paiement inconnu.')\n return { ok: true }\n },\n })\n\n /** Not served by this module yet — said by name, never a dead button. */\n const accountRead = route({\n name: 'storefront.account.read',\n method: 'GET',\n path: '/api/storefront/account',\n auth: 'public',\n handler: () => ({ connecte: false }),\n })\n const accountWrite = route({\n name: 'storefront.account.write',\n method: 'POST',\n path: '/api/storefront/account',\n auth: 'public',\n handler: () => {\n throw refusal(\n 'UNAVAILABLE',\n \"Le compte client n'est pas encore disponible sur cette boutique.\",\n )\n },\n })\n\n return defineModule({\n name: 'commerce',\n routes: [\n storefront,\n visit,\n readCartRoute,\n changeCart,\n checkout,\n callback,\n accountRead,\n accountWrite,\n ] as never,\n })\n}\n","/**\n * The payment, opened at Odoro.\n *\n * A site that sells never holds a payment provider's key: it asks Odoro to\n * open the payment, and Odoro tells it the outcome through the signed callback\n * (`/api/storefront/payment-callback`). The site's secret is its own — shared\n * with Odoro, with no other site — so a compromised site can open payments\n * for itself and nothing else.\n *\n * ## The request's signature\n *\n * `x-odoro-signature` is the hex HMAC-SHA256 of `${timestamp}.${body}`, and\n * `x-odoro-timestamp` the Unix time in seconds. Odoro refuses a signature more\n * than five minutes old: a request copied from a log cannot be replayed.\n *\n * @module\n */\n\nimport { createHmac } from 'node:crypto'\n\nimport { CheckoutError, type PaymentPort } from './checkout.js'\n\nexport interface OdoroPaymentOptions {\n /** Odoro's address, e.g. `https://odoro.ai`. */\n readonly origin: string\n /** This site's identifier at Odoro. */\n readonly site: string\n /** This site's secret, shared with Odoro alone. */\n readonly secret: string\n /** @internal For the tests. */\n readonly fetch?: typeof fetch\n /** @internal For the tests: seconds since the epoch. */\n readonly now?: () => number\n}\n\n/** Where Odoro opens a site's payment. */\nexport const ODORO_PAYMENT_PATH = '/api/payment/site-order'\n\nexport function signRequest(secret: string, timestamp: number, body: string): string {\n return createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex')\n}\n\nconst UNAVAILABLE = \"Le paiement n'a pas pu s'ouvrir. Réessayez dans un instant.\"\n\nexport function odoroPayment(options: OdoroPaymentOptions): PaymentPort {\n const send = options.fetch ?? fetch\n const now = options.now ?? (() => Math.floor(Date.now() / 1000))\n const address = new URL(ODORO_PAYMENT_PATH, options.origin).toString()\n\n return {\n open: async (order) => {\n if (options.secret === '') throw new CheckoutError(409, UNAVAILABLE)\n const body = JSON.stringify({\n site: options.site,\n commande: order.orderId,\n numero: order.number,\n totalCentimes: order.totalCents,\n devise: order.currency,\n courriel: order.email,\n })\n const timestamp = now()\n let reply: Response\n try {\n reply = await send(address, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-odoro-timestamp': String(timestamp),\n 'x-odoro-signature': signRequest(options.secret, timestamp, body),\n },\n body,\n signal: AbortSignal.timeout(15_000),\n })\n } catch {\n throw new CheckoutError(409, UNAVAILABLE)\n }\n const payload = (await reply.json().catch(() => ({}))) as {\n reference?: unknown\n adresse?: unknown\n erreur?: unknown\n }\n if (\n !reply.ok ||\n typeof payload.reference !== 'string' ||\n typeof payload.adresse !== 'string'\n ) {\n // Odoro's refusal is written for the buyer (\"the shop is not open yet\"):\n // it is passed on as it is. Anything else says to try again.\n const said =\n typeof payload.erreur === 'string' && reply.status < 500\n ? payload.erreur\n : UNAVAILABLE\n throw new CheckoutError(409, said)\n }\n return { reference: payload.reference, paymentUrl: payload.adresse }\n },\n }\n}\n","/**\n * A `Base` from a `pg` pool — without depending on `pg`.\n *\n * The shape is structural: any pool whose `query` and `connect` look like\n * node-postgres works, and the site keeps the driver version it chose.\n *\n * A transaction takes ONE client for its whole length: `BEGIN` on one\n * connection and `COMMIT` on another would commit nothing, silently.\n *\n * @module\n */\n\nimport type { Base, Query } from './base.js'\n\ninterface PgResult {\n rows: unknown[]\n}\n\nexport interface PgClientLike {\n query(text: string, values?: readonly unknown[]): Promise<PgResult>\n release(): void\n}\n\nexport interface PgPoolLike {\n query(text: string, values?: readonly unknown[]): Promise<PgResult>\n connect(): Promise<PgClientLike>\n}\n\nfunction reader(\n run: (text: string, values?: readonly unknown[]) => Promise<PgResult>,\n): Query {\n return {\n query: async <Row extends object>(text: string, values?: readonly unknown[]) => {\n const { rows } = await run(text, values)\n return { rows: rows as Row[] }\n },\n }\n}\n\nexport function baseFromPool(pool: PgPoolLike): Base {\n return {\n ...reader((text, values) => pool.query(text, values)),\n transaction: async (work) => {\n const client = await pool.connect()\n try {\n await client.query('BEGIN')\n const result = await work(reader((text, values) => client.query(text, values)))\n await client.query('COMMIT')\n return result\n } catch (cause) {\n await client.query('ROLLBACK').catch(() => undefined)\n throw cause\n } finally {\n client.release()\n }\n },\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/base.ts","../src/images.ts","../src/catalogue.ts","../src/cart.ts","../src/checkout.ts","../src/account.ts","../src/module.ts","../src/odoro-payment.ts","../src/odoro-mail.ts","../src/pg.ts"],"names":["text","count","address","TOKEN","EMAIL","randomBytes","createHmac"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * What the commerce module needs from a database — and nothing more.\n *\n * `query` and `transaction`, the shape `@odoro-cli/cloud-connect` already has:\n * the app wires its connection, and the tests a plain `pg` pool. The module\n * never opens a connection itself, and never reads a connection string: that\n * belongs to the app's configuration.\n *\n * @module\n */\n\n/** Something that runs SQL: a connection, or a transaction. */\nexport interface Query {\n query<Row extends object = Record<string, unknown>>(\n text: string,\n values?: readonly unknown[],\n ): Promise<{ readonly rows: readonly Row[] }>\n}\n\n/** A database the module can also run a transaction on. */\nexport interface Base extends Query {\n transaction<T>(work: (tx: Query) => Promise<T>): Promise<T>\n}\n\n/** The capability version this module speaks. A `shop` 2.x would not be read. */\nexport const SHOP_MAJOR = 1\n\n/**\n * Is the `shop` capability installed, at a version this module reads?\n *\n * Read in the database's own registry (`odoro.features`), which the capability\n * manager writes. Checked once per process: a capability is not uninstalled\n * under a running site.\n */\nexport async function shopInstalled(base: Query): Promise<boolean> {\n try {\n const { rows } = await base.query<{ version: string }>(\n `SELECT version FROM odoro.features WHERE name = 'shop' AND active`,\n )\n const version = rows[0]?.version\n return (\n version !== undefined &&\n Number.parseInt(version.split('.')[0] ?? '', 10) === SHOP_MAJOR\n )\n } catch {\n return false\n }\n}\n","/**\n * Product images, read from the site's own storage (capability `storage`,\n * DECISIONS §44 of the Odoro repository).\n *\n * `shop.product_images.storage_key` names an object of `storage.objects`. The\n * storefront gives an image two ways, as Odoro's central storefront does:\n * inline in `visuel` (base64, so a card renders without a second request),\n * and as bytes for `geste=image` (so a page points at a URL that follows the\n * merchant's latest photo without republishing).\n *\n * A site whose database has no `storage` capability yet keeps working: its\n * images are `null`, as before. That is checked once per process — a\n * capability is not uninstalled under a running site.\n *\n * @module\n */\n\nimport type { Query } from './base.js'\n\n/** SVG can carry script: it is never served, whatever the database says. */\nconst SERVED_TYPES = new Set([\n 'image/png',\n 'image/jpeg',\n 'image/webp',\n 'image/avif',\n 'image/gif',\n])\n\nexport async function storageInstalled(db: Query): Promise<boolean> {\n try {\n const { rows } = await db.query<{ present: boolean }>(\n `SELECT to_regclass('storage.objects') IS NOT NULL AS present`,\n )\n return rows[0]?.present === true\n } catch {\n return false\n }\n}\n\nexport interface StoredImage {\n readonly bytes: Buffer\n readonly type: string\n}\n\n/**\n * The first image of a product FOR SALE, as bytes. `null` for a draft, an\n * unknown id, a product without image, or a type that is never served.\n */\nexport async function productImage(\n db: Query,\n productId: string,\n): Promise<StoredImage | null> {\n if (!/^[0-9a-f-]{36}$/i.test(productId)) return null\n if (!(await storageInstalled(db))) return null\n const { rows } = await db.query<{ bytes: Buffer; mime_type: string }>(\n `SELECT o.bytes, o.mime_type\n FROM shop.product_images i\n JOIN shop.products p ON p.id = i.product_id\n JOIN storage.objects o ON o.key = i.storage_key\n WHERE p.id = $1 AND p.published AND p.deleted_at IS NULL\n ORDER BY i.position, i.id\n LIMIT 1`,\n [productId],\n )\n const row = rows[0]\n if (row === undefined || !SERVED_TYPES.has(row.mime_type)) return null\n return { bytes: row.bytes, type: row.mime_type }\n}\n\n/** The SQL that yields a product's images as base64, when storage exists. */\nexport const SQL_IMAGES = `\n SELECT encode(o.bytes, 'base64') AS contenu, o.mime_type AS type, i.alt_text AS texte\n FROM shop.product_images i\n JOIN storage.objects o ON o.key = i.storage_key\n WHERE i.product_id = p.id AND o.mime_type <> 'image/svg+xml'\n ORDER BY i.position, i.id`\n","/**\n * The catalogue, read from the `shop` schema in the storefront's shapes.\n *\n * Only what is FOR SALE: published, not deleted. A draft is the merchant's\n * business; a visitor who guesses its id gets the same 404 as for a product\n * that never existed.\n *\n * Availability is per VARIANT — the stock lives there — minus the open,\n * unexpired reservations of carts being paid. An expired reservation holds\n * nothing: an abandoned checkout must not take an item off sale forever.\n *\n * Images: `shop.product_images` names objects of the site's `storage`\n * capability (see `images.ts`). They come inline, base64, as in Odoro's\n * central storefront. A database without `storage` keeps `visuel: null`.\n *\n * @module\n */\n\nimport type {\n CatalogueQuery,\n CollectionEnVitrine,\n FicheProduit,\n ProduitEnVitrine,\n VarianteEnVitrine,\n} from '@odoro-cli/commerce'\n\nimport type { Query } from './base.js'\nimport { SQL_IMAGES, storageInstalled } from './images.js'\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/** A variant is available when its stock, minus what is being paid, covers one. */\nexport const SQL_AVAILABLE = `\n (v.stock IS NULL OR v.stock - coalesce((\n SELECT sum(r.quantity)::integer FROM shop.stock_reservations r\n WHERE r.variant_id = v.id AND r.state = 'ouverte' AND r.expires_at > now()\n ), 0) >= 1)`\n\n/** The sort orders a visitor may ask for: a closed list, never a concatenated `ORDER BY`. */\nconst ORDERS: Readonly<Record<NonNullable<CatalogueQuery['tri']>, string>> = {\n nouveautes: 'p.created_at DESC, p.name',\n prix_croissant: 'p.price_cents ASC, p.name',\n prix_decroissant: 'p.price_cents DESC, p.name',\n nom: 'p.name ASC',\n}\n\ninterface ProductRow {\n id: string\n name: string\n description: string\n kind: string\n price_cents: number\n compare_at_cents: number | null\n available: boolean\n /** The first image, when the database has `storage`. */\n visuel?: { contenu: string; type: string; texte: string } | null\n}\n\n/** The first image of each product, as a SELECT column — or a plain null. */\nfunction colonneDuVisuel(avecStockage: boolean): string {\n return avecStockage\n ? `(SELECT json_build_object('contenu', im.contenu, 'type', im.type, 'texte', im.texte)\n FROM (${SQL_IMAGES} LIMIT 1) im) AS visuel`\n : 'NULL::json AS visuel'\n}\n\nfunction toProduct(r: ProductRow): ProduitEnVitrine {\n return {\n id: r.id,\n nom: r.name,\n description: r.description,\n genre: r.kind,\n prixCentimes: Number(r.price_cents),\n prixBarreCentimes: r.compare_at_cents === null ? null : Number(r.compare_at_cents),\n disponible: r.available === true,\n visuel: r.visuel ?? null,\n etiquettes: [],\n prixUnitaire: null,\n }\n}\n\nexport async function readCatalogue(\n db: Query,\n q: CatalogueQuery = {},\n): Promise<{ produits: ProduitEnVitrine[]; total: number }> {\n const text = (q.q ?? '').trim()\n const values: unknown[] = [\n text === '' ? null : `%${text.toLowerCase()}%`,\n q.collection !== undefined && UUID.test(q.collection) ? q.collection : null,\n q.genre ?? null,\n q.prixMin ?? null,\n q.prixMax ?? null,\n q.disponibles === true,\n ]\n const where = `\n FROM shop.products p\n WHERE p.published AND p.deleted_at IS NULL\n AND ($1::text IS NULL OR lower(p.name) LIKE $1 OR lower(p.description) LIKE $1)\n AND ($2::uuid IS NULL OR EXISTS (\n SELECT 1 FROM shop.collection_products cp\n JOIN shop.collections c ON c.id = cp.collection_id AND c.published\n WHERE cp.collection_id = $2 AND cp.product_id = p.id))\n AND ($3::text IS NULL OR p.kind = $3)\n AND ($4::integer IS NULL OR p.price_cents >= $4)\n AND ($5::integer IS NULL OR p.price_cents <= $5)\n AND (NOT $6::boolean OR EXISTS (\n SELECT 1 FROM shop.product_variants v WHERE v.product_id = p.id AND ${SQL_AVAILABLE}))`\n // An unknown order falls back to the merchant's, silently: it is a sort, not an error.\n const order = (q.tri === undefined ? undefined : ORDERS[q.tri]) ?? 'p.position, p.name'\n const limit = Math.min(Math.max(q.combien ?? 24, 1), 60)\n const offset = Math.max(q.depuis ?? 0, 0)\n\n const avecStockage = await storageInstalled(db)\n const { rows } = await db.query<ProductRow>(\n `SELECT p.id, p.name, p.description, p.kind, p.price_cents, p.compare_at_cents,\n EXISTS (SELECT 1 FROM shop.product_variants v WHERE v.product_id = p.id AND ${SQL_AVAILABLE}) AS available,\n ${colonneDuVisuel(avecStockage)}\n ${where}\n ORDER BY ${order}\n LIMIT $7 OFFSET $8`,\n [...values, limit, offset],\n )\n const { rows: count } = await db.query<{ n: number }>(\n `SELECT count(*)::integer AS n ${where}`,\n values,\n )\n return { produits: rows.map(toProduct), total: Number(count[0]?.n ?? 0) }\n}\n\nexport async function readProduct(db: Query, id: string): Promise<FicheProduit | null> {\n if (!UUID.test(id)) return null\n const avecStockage = await storageInstalled(db)\n const { rows } = await db.query<ProductRow>(\n `SELECT p.id, p.name, p.description, p.kind, p.price_cents, p.compare_at_cents,\n EXISTS (SELECT 1 FROM shop.product_variants v WHERE v.product_id = p.id AND ${SQL_AVAILABLE}) AS available,\n ${colonneDuVisuel(avecStockage)}\n FROM shop.products p\n WHERE p.id = $1 AND p.published AND p.deleted_at IS NULL`,\n [id],\n )\n const row = rows[0]\n if (row === undefined) return null\n\n const { rows: options } = await db.query<{\n id: string\n name: string\n values: string[] | null\n }>(\n `SELECT o.id, o.name,\n array_agg(DISTINCT vv.value) FILTER (WHERE vv.value IS NOT NULL) AS values\n FROM shop.product_options o\n LEFT JOIN shop.product_variant_values vv ON vv.option_id = o.id\n WHERE o.product_id = $1\n GROUP BY o.id, o.name, o.position\n ORDER BY o.position, o.name`,\n [id],\n )\n const { rows: variants } = await db.query<{\n id: string\n price_cents: number\n available: boolean\n choices: { option: string; valeur: string }[] | null\n }>(\n `SELECT v.id,\n coalesce(v.price_cents, p.price_cents) AS price_cents,\n ${SQL_AVAILABLE} AS available,\n (SELECT json_agg(json_build_object('option', o.name, 'valeur', vv.value) ORDER BY o.position, o.name)\n FROM shop.product_variant_values vv\n JOIN shop.product_options o ON o.id = vv.option_id\n WHERE vv.variant_id = v.id) AS choices\n FROM shop.product_variants v\n JOIN shop.products p ON p.id = v.product_id\n WHERE v.product_id = $1\n ORDER BY v.position, v.created_at`,\n [id],\n )\n // Every image of the product, in order, for the product page.\n const visuels = avecStockage\n ? (\n await db.query<{ contenu: string; type: string; texte: string }>(\n `SELECT im.contenu, im.type, im.texte FROM (${SQL_IMAGES}) im`.replace(\n 'p.id',\n '$1',\n ),\n [id],\n )\n ).rows.map((v) => ({ contenu: v.contenu, type: v.type, texte: v.texte }))\n : []\n const product = toProduct(row)\n const variantes: VarianteEnVitrine[] = variants.map((v) => {\n const choix = v.choices ?? []\n const price = Number(v.price_cents)\n return {\n id: v.id,\n libelle:\n choix.length === 0\n ? row.name\n : choix.map((c) => `${c.option} : ${c.valeur}`).join(' / '),\n choix,\n prixCentimes: price,\n prixBarreCentimes:\n row.compare_at_cents !== null && row.compare_at_cents > price\n ? Number(row.compare_at_cents)\n : null,\n disponible: v.available === true,\n visuelRang: null,\n prixUnitaire: null,\n }\n })\n return {\n ...product,\n seoTitre: row.name,\n seoResume: row.description,\n visuels,\n options: options.map((o) => ({ id: o.id, nom: o.name, valeurs: o.values ?? [] })),\n variantes,\n }\n}\n\nexport async function readCollections(db: Query): Promise<CollectionEnVitrine[]> {\n const { rows } = await db.query<{\n id: string\n name: string\n description: string\n n: number\n }>(\n `SELECT c.id, c.name, c.description,\n (SELECT count(*)::integer FROM shop.collection_products cp\n JOIN shop.products p ON p.id = cp.product_id\n WHERE cp.collection_id = c.id AND p.published AND p.deleted_at IS NULL) AS n\n FROM shop.collections c\n WHERE c.published\n ORDER BY c.name`,\n )\n return rows.map((c) => ({\n id: c.id,\n nom: c.name,\n description: c.description,\n combien: Number(c.n),\n }))\n}\n","/**\n * The cart, found by its token.\n *\n * The token travels in an `HttpOnly` cookie: no script of the page reads it,\n * so no third-party script injected into a site can copy it elsewhere. It is\n * set on a GESTURE (an add), never on a visit — a site that sets a cookie on\n * every page view would need a consent banner for it.\n *\n * The unit price is recorded at the add: checkout compares it with the price of\n * the day, and says so when it changed.\n *\n * @module\n */\n\nimport { randomBytes } from 'node:crypto'\n\nimport type { LigneDePanier, PanierEnVitrine } from '@odoro-cli/commerce'\n\nimport type { Query } from './base.js'\nimport { SQL_AVAILABLE } from './catalogue.js'\n\nexport const CART_COOKIE = 'odoro_panier'\n/** Thirty days: the window of an abandoned-cart reminder, no longer. */\nexport const CART_MAX_AGE = 60 * 60 * 24 * 30\n\nconst TOKEN = /^vitrine-[0-9a-f]{48}$/\n\nexport function isCartToken(value: string | undefined): value is string {\n return value !== undefined && TOKEN.test(value)\n}\n\nexport function newCartToken(): string {\n return `vitrine-${randomBytes(24).toString('hex')}`\n}\n\n/** Refused gestures carry the sentence the visitor reads. */\nexport class CartError extends Error {}\n\nexport async function findCart(\n db: Query,\n token: string | undefined,\n): Promise<{ id: string; token: string } | null> {\n if (!isCartToken(token)) return null\n const { rows } = await db.query<{ id: string }>(\n `SELECT id FROM shop.carts WHERE token = $1 AND state = 'ouvert' AND NOT preview`,\n [token],\n )\n const row = rows[0]\n return row === undefined ? null : { id: row.id, token }\n}\n\nexport async function openCart(db: Query): Promise<{ id: string; token: string }> {\n const token = newCartToken()\n const { rows } = await db.query<{ id: string }>(\n `INSERT INTO shop.carts (token) VALUES ($1) RETURNING id`,\n [token],\n )\n return { id: rows[0]!.id, token }\n}\n\nasync function touch(db: Query, cartId: string): Promise<void> {\n await db.query('UPDATE shop.carts SET updated_at = now() WHERE id = $1', [cartId])\n}\n\n/**\n * Adds a variant. The join on a PUBLISHED product is the guard: a guessed\n * variant id of a draft adds nothing.\n */\nexport async function addToCart(\n db: Query,\n cartId: string,\n variantId: string,\n quantity: number,\n): Promise<void> {\n const qty = Math.min(Math.max(Math.trunc(Number(quantity) || 1), 1), 99)\n const { rows } = await db.query<{ variant_id: string }>(\n `INSERT INTO shop.cart_lines (cart_id, variant_id, quantity, unit_price_cents)\n SELECT $1, v.id, $3, coalesce(v.price_cents, p.price_cents)\n FROM shop.product_variants v\n JOIN shop.products p ON p.id = v.product_id\n WHERE v.id = $2 AND p.published AND p.deleted_at IS NULL\n ON CONFLICT (cart_id, variant_id)\n DO UPDATE SET quantity = least(shop.cart_lines.quantity + excluded.quantity, 99)\n RETURNING variant_id`,\n [cartId, variantId, qty],\n )\n if (rows.length === 0) throw new CartError(\"Cet article n'est plus en vente.\")\n await touch(db, cartId)\n}\n\n/** A quantity. Zero removes the line — the gesture the screen expects. */\nexport async function setQuantity(\n db: Query,\n cartId: string,\n variantId: string,\n quantity: number,\n): Promise<void> {\n const qty = Math.min(Math.max(Math.trunc(Number(quantity) || 0), 0), 99)\n if (qty === 0) {\n await db.query('DELETE FROM shop.cart_lines WHERE cart_id = $1 AND variant_id = $2', [\n cartId,\n variantId,\n ])\n } else {\n await db.query(\n 'UPDATE shop.cart_lines SET quantity = $3 WHERE cart_id = $1 AND variant_id = $2',\n [cartId, variantId, qty],\n )\n }\n await touch(db, cartId)\n}\n\nexport async function readCart(\n db: Query,\n cart: { id: string; token: string },\n isNew = false,\n): Promise<PanierEnVitrine> {\n const { rows } = await db.query<{\n variant_id: string\n product_id: string\n name: string\n label: string | null\n unit_price_cents: number\n quantity: number\n available: boolean\n }>(\n `SELECT l.variant_id, v.product_id, p.name,\n (SELECT string_agg(vv.value, ' / ' ORDER BY o.position)\n FROM shop.product_variant_values vv\n JOIN shop.product_options o ON o.id = vv.option_id\n WHERE vv.variant_id = v.id) AS label,\n l.unit_price_cents, l.quantity,\n (p.published AND p.deleted_at IS NULL AND ${SQL_AVAILABLE}) AS available\n FROM shop.cart_lines l\n JOIN shop.product_variants v ON v.id = l.variant_id\n JOIN shop.products p ON p.id = v.product_id\n WHERE l.cart_id = $1\n ORDER BY l.added_at, l.variant_id`,\n [cart.id],\n )\n const { rows: head } = await db.query<{ email: string | null }>(\n 'SELECT email FROM shop.carts WHERE id = $1',\n [cart.id],\n )\n const lignes: LigneDePanier[] = rows.map((l) => ({\n varianteId: l.variant_id,\n produitId: l.product_id,\n nom: l.name,\n declinaison: l.label ?? '',\n prixUnitaireCentimes: Number(l.unit_price_cents),\n quantite: Number(l.quantity),\n sousTotalCentimes: Number(l.unit_price_cents) * Number(l.quantity),\n disponible: l.available === true,\n visuel: null,\n }))\n return {\n panierId: cart.id,\n jeton: cart.token,\n courriel: head[0]?.email ?? null,\n lignes,\n combien: lignes.reduce((n, l) => n + l.quantite, 0),\n sousTotalCentimes: lignes.reduce((n, l) => n + l.sousTotalCentimes, 0),\n neuf: isNew,\n }\n}\n","/**\n * Checkout: the order lives in the site's database, the MONEY does not.\n *\n * Decided with the founder of Odoro (DECISIONS §43 of the main repository):\n * the payment stays with Odoro (Whop), which knows money and its obligations.\n * This module opens the order, reserves the stock, and hands the payment to a\n * PORT the app wires — Odoro's payment API. The order only keeps the opaque\n * reference the payment returned; it learns the outcome through a SIGNED\n * callback (`confirmPayment`), never from the visitor's browser.\n *\n * The three gestures of the storefront contract:\n *\n * · `ouvrir` — reprices the cart at today's prices (and says what changed),\n * checks and reserves the stock for thirty minutes, writes the order;\n * · `chiffrer` — the total, with the discount code the visitor typed. The\n * code is priced HERE, from the shop's own `discount_codes` (shop 1.2.0,\n * copied from Odoro): the browser only names it. An unknown code is\n * refused by name, never silently ignored;\n * · `payer` — the payment page, from the port.\n *\n * @module\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto'\n\nimport type { Base, Query } from './base.js'\nimport { CartError } from './cart.js'\n\n/** A refused checkout gesture: `status` and the sentence the visitor reads. */\nexport class CheckoutError extends Error {\n constructor(\n readonly status: 400 | 404 | 409,\n message: string,\n ) {\n super(message)\n }\n}\n\n/** What the visitor typed, under the storefront contract's names. */\nexport interface CheckoutInput {\n courriel: string\n nom: string\n ligne1: string\n ligne2?: string\n code_postal: string\n ville: string\n pays: string\n telephone?: string\n}\n\n/** Where the money goes: Odoro's payment, wired by the app. */\nexport interface PaymentPort {\n open(input: {\n readonly orderId: string\n readonly number: number\n readonly totalCents: number\n readonly currency: string\n readonly email: string\n }): Promise<{ readonly reference: string; readonly paymentUrl: string }>\n}\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n/** How long a checkout holds the stock. */\nconst RESERVATION_MINUTES = 30\n\nfunction need(value: string | undefined, message: string): string {\n const v = (value ?? '').trim()\n if (v === '') throw new CheckoutError(400, message)\n return v\n}\n\ninterface OrderRow {\n id: string\n number: string | number\n state: string\n email: string\n currency: string\n subtotal_cents: number\n shipping_cents: number\n tax_cents: number\n total_cents: number\n /** Absent on a shop still in 1.1.0 — see `codesInstalled`. */\n discount_code?: string | null\n discount_cents?: number\n payment_ref: string | null\n}\n\n/**\n * Are the discount codes installed (shop 1.2.0)? A shop still in 1.1.0 keeps\n * selling: a code is refused by name there, as before this module served\n * them, and nothing reads a column the shop does not have.\n */\nexport async function codesInstalled(db: Query): Promise<boolean> {\n try {\n const { rows } = await db.query<{ present: boolean }>(\n `SELECT to_regclass('shop.discount_codes') IS NOT NULL AS present`,\n )\n return rows[0]?.present === true\n } catch {\n return false\n }\n}\n\nexport async function quote(db: Query, orderId: string) {\n const { rows } = await db.query<OrderRow>('SELECT * FROM shop.orders WHERE id = $1', [\n orderId,\n ])\n const order = rows[0]\n if (order === undefined) throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n const { rows: lines } = await db.query<{\n variant_id: string | null\n product_id: string | null\n product_name: string\n unit_price_cents: number\n quantity: number\n }>(\n `SELECT l.variant_id, v.product_id, l.product_name, l.unit_price_cents, l.quantity\n FROM shop.order_lines l LEFT JOIN shop.product_variants v ON v.id = l.variant_id\n WHERE l.order_id = $1 ORDER BY l.product_name`,\n [orderId],\n )\n return {\n caisseId: order.id,\n devise: order.currency,\n lignes: lines.map((l) => ({\n varianteId: l.variant_id ?? '',\n produitId: l.product_id ?? '',\n nom: l.product_name,\n genre: 'physique',\n prixUnitaireCentimes: Number(l.unit_price_cents),\n quantite: Number(l.quantity),\n sousTotalCentimes: Number(l.unit_price_cents) * Number(l.quantity),\n taxable: true,\n })),\n remises:\n (order.discount_code ?? null) === null\n ? []\n : [\n {\n remiseId: null,\n code: order.discount_code ?? '',\n libelle: order.discount_code ?? '',\n montantCentimes: Number(order.discount_cents ?? 0),\n },\n ],\n carteCadeau: null,\n methodeDeLivraison: null,\n sousTotalCentimes: Number(order.subtotal_cents),\n remiseCentimes: Number(order.discount_cents ?? 0),\n portCentimes: Number(order.shipping_cents),\n taxeCentimes: Number(order.tax_cents),\n carteCadeauCentimes: 0,\n totalCentimes: Number(order.total_cents),\n taxeIncluse: true,\n }\n}\n\nexport async function openCheckout(\n db: Base,\n cartId: string,\n input: CheckoutInput,\n currency = 'EUR',\n): Promise<{\n orderId: string\n repriced: { varianteId: string; ancienCentimes: number; nouveauCentimes: number }[]\n}> {\n const email = need(input.courriel, 'Indiquez votre adresse e-mail.')\n if (!EMAIL.test(email))\n throw new CheckoutError(400, \"Cette adresse e-mail n'est pas valable.\")\n const name = need(input.nom, 'Indiquez votre nom.')\n const address = {\n ligne1: need(input.ligne1, 'Indiquez votre adresse.'),\n ligne2: (input.ligne2 ?? '').trim(),\n code_postal: need(input.code_postal, 'Indiquez votre code postal.'),\n ville: need(input.ville, 'Indiquez votre ville.'),\n pays: need(input.pays, 'Indiquez votre pays.'),\n telephone: (input.telephone ?? '').trim(),\n }\n\n return await db.transaction(async (tx) => {\n // A checkout reopened on the same cart replaces the previous one: its\n // reservation is released, and only one open order stays.\n await tx.query(\n `UPDATE shop.stock_reservations SET state = 'relachee'\n WHERE cart_id = $1 AND state = 'ouverte'`,\n [cartId],\n )\n await tx.query(\n `UPDATE shop.orders SET state = 'annulee'\n WHERE cart_id = $1 AND state = 'ouverte' AND payment_ref IS NULL`,\n [cartId],\n )\n\n const { rows: lines } = await tx.query<{\n variant_id: string\n quantity: number\n unit_price_cents: number\n current_cents: number\n stock: number | null\n for_sale: boolean\n name: string\n label: string | null\n }>(\n `SELECT l.variant_id, l.quantity, l.unit_price_cents,\n coalesce(v.price_cents, p.price_cents) AS current_cents,\n v.stock, (p.published AND p.deleted_at IS NULL) AS for_sale, p.name,\n (SELECT string_agg(vv.value, ' / ' ORDER BY o.position)\n FROM shop.product_variant_values vv JOIN shop.product_options o ON o.id = vv.option_id\n WHERE vv.variant_id = v.id) AS label\n FROM shop.cart_lines l\n JOIN shop.product_variants v ON v.id = l.variant_id\n JOIN shop.products p ON p.id = v.product_id\n WHERE l.cart_id = $1\n ORDER BY l.variant_id\n FOR UPDATE OF v`,\n [cartId],\n )\n if (lines.length === 0) throw new CartError('Votre panier est vide.')\n const gone = lines.find((l) => !l.for_sale)\n if (gone !== undefined) {\n throw new CheckoutError(\n 409,\n `« ${gone.name} » n'est plus en vente : retirez-le de votre panier.`,\n )\n }\n\n for (const l of lines) {\n if (l.stock === null) continue\n const { rows } = await tx.query<{ held: number }>(\n `SELECT coalesce(sum(quantity), 0)::integer AS held FROM shop.stock_reservations\n WHERE variant_id = $1 AND state = 'ouverte' AND expires_at > now()`,\n [l.variant_id],\n )\n if (Number(l.stock) - Number(rows[0]?.held ?? 0) < Number(l.quantity)) {\n throw new CheckoutError(409, `« ${l.name} » : il n'en reste pas assez.`)\n }\n }\n\n const repriced = lines\n .filter((l) => Number(l.current_cents) !== Number(l.unit_price_cents))\n .map((l) => ({\n varianteId: l.variant_id,\n ancienCentimes: Number(l.unit_price_cents),\n nouveauCentimes: Number(l.current_cents),\n }))\n for (const r of repriced) {\n await tx.query(\n 'UPDATE shop.cart_lines SET unit_price_cents = $3 WHERE cart_id = $1 AND variant_id = $2',\n [cartId, r.varianteId, r.nouveauCentimes],\n )\n }\n\n const subtotal = lines.reduce(\n (n, l) => n + Number(l.current_cents) * Number(l.quantity),\n 0,\n )\n const { rows: created } = await tx.query<{ id: string }>(\n `INSERT INTO shop.orders (cart_id, email, customer_name, shipping_address, currency, subtotal_cents, total_cents)\n VALUES ($1, $2, $3, $4, $5, $6, $6) RETURNING id`,\n [cartId, email, name, JSON.stringify(address), currency, subtotal],\n )\n const orderId = created[0]!.id\n for (const l of lines) {\n await tx.query(\n `INSERT INTO shop.order_lines (order_id, variant_id, product_name, variant_label, unit_price_cents, quantity)\n VALUES ($1, $2, $3, $4, $5, $6)`,\n [orderId, l.variant_id, l.name, l.label ?? '', l.current_cents, l.quantity],\n )\n if (l.stock !== null) {\n await tx.query(\n `INSERT INTO shop.stock_reservations (variant_id, cart_id, quantity, expires_at)\n VALUES ($1, $2, $3, now() + make_interval(mins => $4))`,\n [l.variant_id, cartId, l.quantity, RESERVATION_MINUTES],\n )\n }\n }\n await tx.query('UPDATE shop.carts SET email = $2, updated_at = now() WHERE id = $1', [\n cartId,\n email,\n ])\n return { orderId, repriced }\n })\n}\n\n/**\n * What a code takes off a subtotal. Rounded DOWN: a rounding never gives the\n * buyer a cent the seller did not offer. Never more than the subtotal.\n *\n * A percentage is counted in thousandths of a percent (10000 = 10 %), as Odoro\n * counts it — the codes are copied from there unchanged.\n */\nexport function discountFor(\n kind: 'pourcentage' | 'montant',\n value: number,\n subtotalCents: number,\n): number {\n const raw =\n kind === 'pourcentage' ? Math.floor((subtotalCents * value) / 100_000) : value\n return Math.max(0, Math.min(raw, subtotalCents))\n}\n\nfunction euros(cents: number, currency: string): string {\n return new Intl.NumberFormat('fr', { style: 'currency', currency }).format(cents / 100)\n}\n\n/**\n * The quote with the code the visitor typed — or without, when the field is\n * empty: every `chiffrer` and every `payer` names the code, and the order\n * carries exactly that one.\n *\n * Once the payment is open, the total is frozen: Odoro refuses a payment\n * whose amount changed under the same order. Naming the same code again is\n * fine; another one asks to reopen the checkout.\n */\nexport async function applyCode(db: Query, orderId: string, code: string | undefined) {\n const wanted = (code ?? '').trim()\n if (!(await codesInstalled(db))) {\n if (wanted !== '')\n throw new CheckoutError(409, \"Ce code n'est pas reconnu par cette boutique.\")\n return await quote(db, orderId)\n }\n const { rows } = await db.query<OrderRow>('SELECT * FROM shop.orders WHERE id = $1', [\n orderId,\n ])\n const order = rows[0]\n if (order === undefined) throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n if (order.state !== 'ouverte' || order.payment_ref !== null) {\n if (wanted.toUpperCase() === (order.discount_code ?? '').toUpperCase())\n return await quote(db, orderId)\n throw new CheckoutError(\n 409,\n 'Le paiement de cette commande est déjà ouvert : rouvrez la caisse pour changer de code.',\n )\n }\n\n const subtotal = Number(order.subtotal_cents)\n let discount = 0\n let applied: string | null = null\n if (wanted !== '') {\n const { rows: found } = await db.query<{\n id: string\n code: string\n kind: 'pourcentage' | 'montant'\n value: number\n min_subtotal_cents: number\n max_uses: number | null\n used: number\n }>(\n `SELECT c.id, c.code, c.kind, c.value, c.min_subtotal_cents, c.max_uses,\n (SELECT count(*) FROM shop.discount_uses u WHERE u.discount_id = c.id)::integer AS used\n FROM shop.discount_codes c\n WHERE upper(c.code) = upper($1) AND c.active\n AND (c.starts_at IS NULL OR c.starts_at <= now())\n AND (c.ends_at IS NULL OR c.ends_at > now())`,\n [wanted],\n )\n const found0 = found[0]\n if (found0 === undefined)\n throw new CheckoutError(409, \"Ce code n'est pas reconnu par cette boutique.\")\n if (found0.max_uses !== null && Number(found0.used) >= Number(found0.max_uses))\n throw new CheckoutError(409, 'Ce code a déjà servi autant de fois que prévu.')\n if (subtotal < Number(found0.min_subtotal_cents))\n throw new CheckoutError(\n 409,\n `Ce code demande un panier d'au moins ${euros(Number(found0.min_subtotal_cents), order.currency)}.`,\n )\n discount = discountFor(found0.kind, Number(found0.value), subtotal)\n // Nothing is free here: a payment of zero cannot be opened, and a code\n // that erased the whole order would leave the visitor at a dead end.\n if (subtotal - discount + Number(order.shipping_cents) + Number(order.tax_cents) <= 0)\n throw new CheckoutError(409, 'Ce code ne peut pas rendre la commande gratuite.')\n applied = found0.code\n }\n\n await db.query(\n `UPDATE shop.orders\n SET discount_code = $2, discount_cents = $3,\n total_cents = subtotal_cents - $3 + shipping_cents + tax_cents\n WHERE id = $1 AND state = 'ouverte' AND payment_ref IS NULL`,\n [orderId, applied, discount],\n )\n return await quote(db, orderId)\n}\n\nexport async function pay(db: Query, orderId: string, port: PaymentPort) {\n const { rows } = await db.query<OrderRow & { expired: boolean }>(\n `SELECT o.*,\n EXISTS (SELECT 1 FROM shop.stock_reservations r\n WHERE r.cart_id = o.cart_id AND r.state = 'ouverte' AND r.expires_at <= now()) AS expired\n FROM shop.orders o WHERE o.id = $1`,\n [orderId],\n )\n const order = rows[0]\n if (order === undefined) throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n if (order.state !== 'ouverte')\n throw new CheckoutError(409, 'Cette commande ne peut plus être payée.')\n if (order.expired)\n throw new CheckoutError(409, 'Votre réservation a expiré : rouvrez la caisse.')\n\n const opened = await port.open({\n orderId: order.id,\n number: Number(order.number),\n totalCents: Number(order.total_cents),\n currency: order.currency,\n email: order.email,\n })\n await db.query('UPDATE shop.orders SET payment_ref = $2 WHERE id = $1', [\n order.id,\n opened.reference,\n ])\n const total = Number(order.total_cents)\n return {\n commandeId: order.id,\n numero: Number(order.number),\n jetonDeSuivi: order.id,\n totalCentimes: total,\n totalTexte: new Intl.NumberFormat('fr', {\n style: 'currency',\n currency: order.currency,\n }).format(total / 100),\n adresseDePaiement: opened.paymentUrl,\n }\n}\n\n/** How long a signed callback stays valid. */\nconst CALLBACK_WINDOW_S = 300\n\n/** The signature of a payment callback: HMAC-SHA256 of `reference.state.timestamp`. */\nexport function signCallback(\n secret: string,\n reference: string,\n state: string,\n timestamp: number,\n): string {\n return createHmac('sha256', secret)\n .update(`${reference}.${state}.${timestamp}`)\n .digest('hex')\n}\n\nexport function verifyCallback(\n secret: string,\n input: { reference: string; etat: string; horodatage: number; signature: string },\n now = Date.now(),\n): boolean {\n if (secret === '' || Math.abs(now / 1000 - input.horodatage) > CALLBACK_WINDOW_S)\n return false\n const expected = Buffer.from(\n signCallback(secret, input.reference, input.etat, input.horodatage),\n )\n const received = Buffer.from(input.signature)\n return expected.length === received.length && timingSafeEqual(expected, received)\n}\n\n/**\n * The payment's outcome, from Odoro. Idempotent: a callback delivered twice\n * changes nothing the second time.\n */\nexport async function confirmPayment(\n db: Base,\n reference: string,\n state: 'payee' | 'echouee',\n): Promise<boolean> {\n return await db.transaction(async (tx) => {\n const { rows } = await tx.query<{\n id: string\n cart_id: string | null\n state: string\n }>('SELECT id, cart_id, state FROM shop.orders WHERE payment_ref = $1 FOR UPDATE', [\n reference,\n ])\n const order = rows[0]\n if (order === undefined) return false\n if (order.state !== 'ouverte') return true\n\n if (state === 'echouee') {\n await tx.query(`UPDATE shop.orders SET state = 'echouee' WHERE id = $1`, [order.id])\n await tx.query(\n `UPDATE shop.stock_reservations SET state = 'relachee' WHERE cart_id = $1 AND state = 'ouverte'`,\n [order.cart_id],\n )\n return true\n }\n\n await tx.query(\n `UPDATE shop.orders SET state = 'payee', paid_at = now() WHERE id = $1`,\n [order.id],\n )\n // A code counts once it is PAID for: an abandoned checkout uses nothing.\n if (await codesInstalled(tx))\n await tx.query(\n `INSERT INTO shop.discount_uses (order_id, discount_id)\n SELECT o.id, c.id FROM shop.orders o\n JOIN shop.discount_codes c ON upper(c.code) = upper(o.discount_code)\n WHERE o.id = $1\n ON CONFLICT (order_id) DO NOTHING`,\n [order.id],\n )\n // The stock leaves when the money arrives, not before.\n await tx.query(\n `UPDATE shop.product_variants v\n SET stock = greatest(v.stock - l.quantity, 0)\n FROM shop.order_lines l\n WHERE l.order_id = $1 AND l.variant_id = v.id AND v.stock IS NOT NULL`,\n [order.id],\n )\n await tx.query(\n `UPDATE shop.stock_reservations SET state = 'consommee' WHERE cart_id = $1 AND state = 'ouverte'`,\n [order.cart_id],\n )\n await tx.query(\n `UPDATE shop.carts SET state = 'commande', updated_at = now() WHERE id = $1`,\n [order.cart_id],\n )\n return true\n })\n}\n\n/** An order's state, for the tracking link the buyer received. */\nexport async function orderState(db: Query, orderId: string) {\n if (!/^[0-9a-f-]{36}$/i.test(orderId)) return null\n const { rows } = await db.query<OrderRow & { created_at: Date }>(\n 'SELECT * FROM shop.orders WHERE id = $1',\n [orderId],\n )\n const order = rows[0]\n if (order === undefined) return null\n const { rows: lines } = await db.query<{\n product_name: string\n quantity: number\n unit_price_cents: number\n }>(\n 'SELECT product_name, quantity, unit_price_cents FROM shop.order_lines WHERE order_id = $1 ORDER BY product_name',\n [orderId],\n )\n return {\n numero: Number(order.number),\n etat: order.state,\n courriel: order.email,\n totalCentimes: Number(order.total_cents),\n passeeLe: new Date(order.created_at).toISOString(),\n lignes: lines.map((l) => ({\n nom: l.product_name,\n quantite: Number(l.quantity),\n sousTotalCentimes: Number(l.unit_price_cents) * Number(l.quantity),\n })),\n livraisons: [],\n }\n}\n","/**\n * The customer's account, in the shop's own database (shop 1.3.0).\n *\n * No password: a fifteen-minute link, sent to the address. The link and the\n * session live in the database as FINGERPRINTS only (SHA-256): a database\n * that is read connects nobody.\n *\n * The same contract as Odoro's central storefront, so the site's account\n * page works unchanged:\n *\n * POST { geste: 'lien', email, offres? } create the account / sign in:\n * a link leaves by e-mail;\n * POST { geste: 'lettre', email, offres } the newsletter — `offres` MUST be\n * true (the ticked box);\n * POST { geste: 'profil', nom?, offres? } the profile (session required);\n * POST { geste: 'deconnexion' } closes the session;\n * GET the profile, the orders, the addresses.\n *\n * `lien` and `lettre` always answer `{ ok: true }`: the form must not become\n * a directory of the shop's customers. At most three links per person per\n * hour: without that bound, the page would fill someone's mailbox.\n *\n * The link itself is opened by `openSession` — mounted by the app on\n * `/api/storefront/account/login`, because it answers with a redirect and\n * cookies, not with JSON.\n *\n * @module\n */\n\nimport { createHash, randomBytes } from 'node:crypto'\n\nimport type { Base, Query } from './base.js'\nimport { CheckoutError } from './checkout.js'\n\n/** The session cookie: HttpOnly, the browser's script never reads it. */\nexport const CUSTOMER_COOKIE = 'odoro_client'\n/** How long a session lasts, in seconds. */\nexport const SESSION_MAX_AGE = 90 * 24 * 3600\n/** How long a link in an e-mail stays valid. */\nconst LINK_MINUTES = 15\n/** Links per person per hour. */\nexport const LINKS_PER_HOUR = 3\n/** The shape of a token: what `randomBytes(32).toString('base64url')` gives. */\nconst TOKEN = /^[A-Za-z0-9_-]{20,80}$/\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** Where the sign-in link leaves: Odoro sends it, the shop holds no mail key. */\nexport interface MailPort {\n sendLoginLink(input: { readonly email: string; readonly token: string }): Promise<void>\n}\n\n/** Are the accounts installed (shop 1.3.0)? */\nexport async function accountsInstalled(db: Query): Promise<boolean> {\n try {\n const { rows } = await db.query<{ present: boolean }>(\n `SELECT to_regclass('shop.customers') IS NOT NULL AS present`,\n )\n return rows[0]?.present === true\n } catch {\n return false\n }\n}\n\nfunction fingerprint(token: string): string {\n return createHash('sha256').update(token).digest('hex')\n}\n\nfunction newToken(): string {\n return randomBytes(32).toString('base64url')\n}\n\nexport interface Customer {\n readonly id: string\n readonly email: string\n readonly name: string\n readonly consent: boolean\n}\n\n/**\n * Creates the account if it does not exist, and records the consent when it\n * is GIVEN — an unticked box never withdraws one given before: that is the\n * profile's job, where the person sees what they change.\n */\nasync function register(\n db: Query,\n email: string,\n input: { name?: string; consent: boolean; source: 'compte' | 'lettre' },\n): Promise<string> {\n const { rows } = await db.query<{ id: string }>(\n `INSERT INTO shop.customers (email, name, marketing_consent, consent_source, consent_at)\n VALUES ($1, $2, $3, CASE WHEN $3 THEN $4 END, CASE WHEN $3 THEN now() END)\n ON CONFLICT (email) DO UPDATE SET\n name = CASE WHEN excluded.name <> '' THEN excluded.name ELSE shop.customers.name END,\n marketing_consent = shop.customers.marketing_consent OR excluded.marketing_consent,\n consent_source = CASE WHEN excluded.marketing_consent AND NOT shop.customers.marketing_consent\n THEN excluded.consent_source ELSE shop.customers.consent_source END,\n consent_at = CASE WHEN excluded.marketing_consent AND NOT shop.customers.marketing_consent\n THEN now() ELSE shop.customers.consent_at END\n RETURNING id`,\n [email, (input.name ?? '').trim().slice(0, 120), input.consent, input.source],\n )\n return rows[0]!.id\n}\n\nfunction address(raw: string | undefined): string {\n const email = (raw ?? '').trim().toLowerCase().slice(0, 254)\n if (!EMAIL.test(email))\n throw new CheckoutError(400, \"Cette adresse de courriel n'est pas valable.\")\n return email\n}\n\n/** `lien`: the account, and a link by e-mail — unless three already left this hour. */\nexport async function requestLink(\n db: Query,\n mail: MailPort,\n input: { email?: string; name?: string; consent?: boolean },\n): Promise<void> {\n const email = address(input.email)\n const id = await register(db, email, {\n ...(input.name === undefined ? {} : { name: input.name }),\n consent: input.consent === true,\n source: 'compte',\n })\n const { rows } = await db.query<{ n: number }>(\n `SELECT count(*)::integer AS n FROM shop.customer_links\n WHERE customer_id = $1 AND created_at > now() - interval '1 hour'`,\n [id],\n )\n if (Number(rows[0]?.n ?? 0) >= LINKS_PER_HOUR) return\n const token = newToken()\n await db.query(\n `INSERT INTO shop.customer_links (customer_id, fingerprint, expires_at)\n VALUES ($1, $2, now() + make_interval(mins => $3))`,\n [id, fingerprint(token), LINK_MINUTES],\n )\n try {\n await mail.sendLoginLink({ email, token })\n } catch {\n // The answer stays the same: it must teach nothing to whoever tries.\n }\n}\n\n/** `lettre`: the newsletter, on the ticked box only. */\nexport async function subscribe(\n db: Query,\n input: { email?: string; name?: string; consent?: boolean },\n): Promise<void> {\n if (input.consent !== true) throw new CheckoutError(400, 'Requête illisible.')\n await register(db, address(input.email), {\n ...(input.name === undefined ? {} : { name: input.name }),\n consent: true,\n source: 'lettre',\n })\n}\n\n/**\n * The link from the e-mail, opened: it serves ONCE. Returns the session to\n * set, and the cart to set when the person had one elsewhere — or `null`\n * when the link is unknown, used or expired.\n *\n * The cart follows the person: this browser's cart becomes theirs; if this\n * browser has none, their last open cart comes back.\n */\nexport async function openSession(\n db: Base,\n token: string,\n cartToken: string | undefined,\n): Promise<{ session: string; cart: string | null } | null> {\n if (!TOKEN.test(token)) return null\n return await db.transaction(async (tx) => {\n const { rows } = await tx.query<{ customer_id: string }>(\n `UPDATE shop.customer_links SET used_at = now()\n WHERE fingerprint = $1 AND used_at IS NULL AND expires_at > now()\n RETURNING customer_id`,\n [fingerprint(token)],\n )\n const customer = rows[0]?.customer_id\n if (customer === undefined) return null\n const session = newToken()\n await tx.query(\n `INSERT INTO shop.customer_sessions (customer_id, fingerprint, expires_at)\n VALUES ($1, $2, now() + make_interval(secs => $3))`,\n [customer, fingerprint(session), SESSION_MAX_AGE],\n )\n await tx.query('UPDATE shop.customers SET last_seen_at = now() WHERE id = $1', [\n customer,\n ])\n\n if (cartToken !== undefined && cartToken !== '') {\n const { rows: mine } = await tx.query<{ id: string }>(\n `UPDATE shop.carts SET customer_id = $2, updated_at = now()\n WHERE token = $1 AND state = 'ouvert' RETURNING id`,\n [cartToken, customer],\n )\n if (mine.length > 0) return { session, cart: null }\n }\n const { rows: last } = await tx.query<{ token: string }>(\n `SELECT token FROM shop.carts\n WHERE customer_id = $1 AND state = 'ouvert'\n ORDER BY updated_at DESC LIMIT 1`,\n [customer],\n )\n return { session, cart: last[0]?.token ?? null }\n })\n}\n\n/** The person a session cookie names, or `null`. */\nexport async function customerOf(\n db: Query,\n session: string | undefined,\n): Promise<Customer | null> {\n if (session === undefined || !TOKEN.test(session)) return null\n const { rows } = await db.query<{\n id: string\n email: string\n name: string\n marketing_consent: boolean\n }>(\n `SELECT c.id, c.email, c.name, c.marketing_consent\n FROM shop.customer_sessions s JOIN shop.customers c ON c.id = s.customer_id\n WHERE s.fingerprint = $1 AND s.expires_at > now()`,\n [fingerprint(session)],\n )\n const c = rows[0]\n return c === undefined\n ? null\n : { id: c.id, email: c.email, name: c.name, consent: c.marketing_consent }\n}\n\n/** The profile, as the site's account page reads it. */\nexport async function profile(db: Query, customer: Customer) {\n // The orders are found by ADDRESS: the link proved the person holds it,\n // and orders placed before the account existed are theirs too.\n const { rows: orders } = await db.query<{\n id: string\n state: string\n total_cents: number\n currency: string\n created_at: Date\n paid_at: Date | null\n shipping_address: Record<string, string> | null\n customer_name: string\n }>(\n `SELECT id, state, total_cents, currency, created_at, paid_at, shipping_address, customer_name\n FROM shop.orders\n WHERE lower(email) = $1 AND state IN ('payee', 'remboursee', 'annulee')\n ORDER BY created_at DESC LIMIT 50`,\n [customer.email],\n )\n const seen = new Set<string>()\n const addresses: { nom: string | null; lignes: string[] }[] = []\n for (const o of orders) {\n const a = o.shipping_address ?? {}\n const lignes = [\n a['ligne1'],\n a['ligne2'],\n `${a['code_postal'] ?? ''} ${a['ville'] ?? ''}`.trim(),\n a['pays'],\n ].filter((x): x is string => typeof x === 'string' && x !== '')\n const key = lignes.join('|')\n if (lignes.length === 0 || seen.has(key) || addresses.length >= 5) continue\n seen.add(key)\n addresses.push({ nom: o.customer_name || null, lignes })\n }\n return {\n nom: customer.name === '' ? null : customer.name,\n courriel: customer.email,\n offres: customer.consent,\n commandes: orders.map((o) => ({\n id: o.id,\n etat: o.state,\n total: new Intl.NumberFormat('fr', {\n style: 'currency',\n currency: o.currency,\n }).format(Number(o.total_cents) / 100),\n le: new Date(o.paid_at ?? o.created_at).toISOString(),\n })),\n adresses: addresses,\n }\n}\n\n/** `profil`: the name, and the consent — which the person CAN withdraw here. */\nexport async function updateProfile(\n db: Query,\n customer: Customer,\n input: { name?: string; consent?: boolean },\n): Promise<void> {\n await db.query(\n `UPDATE shop.customers SET\n name = coalesce($2, name),\n marketing_consent = coalesce($3, marketing_consent),\n consent_source = CASE WHEN $3 IS TRUE AND NOT marketing_consent THEN 'compte'\n WHEN $3 IS FALSE THEN NULL ELSE consent_source END,\n consent_at = CASE WHEN $3 IS TRUE AND NOT marketing_consent THEN now()\n WHEN $3 IS FALSE THEN NULL ELSE consent_at END,\n last_seen_at = now()\n WHERE id = $1`,\n [\n customer.id,\n input.name === undefined ? null : input.name.trim().slice(0, 120),\n input.consent === undefined ? null : input.consent,\n ],\n )\n}\n\n/** `deconnexion`: the session ends here, not only in this browser. */\nexport async function closeSession(\n db: Query,\n session: string | undefined,\n): Promise<void> {\n if (session === undefined || !TOKEN.test(session)) return\n await db.query('DELETE FROM shop.customer_sessions WHERE fingerprint = $1', [\n fingerprint(session),\n ])\n}\n","/**\n * The storefront contract, as an `@odoro-cli/server` module.\n *\n * A site that sells, running in its own container with its own database,\n * mounts this module and serves `/api/storefront/*` itself — the same answers\n * as Odoro's central storefront, so the V4 script (`odoro-boutique.js`) and\n * `@odoro-cli/commerce` talk to it without knowing which one they reach.\n *\n * ## What it serves, and what it does not yet\n *\n * The catalogue, product pages, collections, the cart, checkout (open, quote\n * with the shop's discount codes, pay through Odoro), order tracking, and the\n * customer's account (shop 1.3.0, with a `mail` port). What a shop cannot\n * serve answers so, by name — an account on a shop without accounts says the\n * account is not available, an unknown code says it is not recognised. A\n * gesture that silently does nothing is worse than one that says no.\n *\n * ## Errors speak the contract\n *\n * The storefront's clients read `erreur`. Every refusal is a problem document\n * (RFC 9457, like the rest of the kernel) that also carries `erreur`.\n *\n * @module\n */\n\nimport {\n ApiError,\n ConflictError,\n NotFoundError,\n ServiceUnavailableError,\n defineModule,\n route,\n type Cookies,\n} from '@odoro-cli/server'\nimport { z } from 'zod'\n\nimport {\n CUSTOMER_COOKIE,\n accountsInstalled,\n closeSession,\n customerOf,\n profile,\n requestLink,\n subscribe,\n updateProfile,\n type MailPort,\n} from './account.js'\nimport { type Base, shopInstalled } from './base.js'\nimport {\n CART_COOKIE,\n CART_MAX_AGE,\n CartError,\n addToCart,\n findCart,\n openCart,\n readCart,\n setQuantity,\n} from './cart.js'\nimport { readCatalogue, readCollections, readProduct } from './catalogue.js'\nimport {\n CheckoutError,\n applyCode,\n confirmPayment,\n openCheckout,\n orderState,\n pay,\n quote,\n verifyCallback,\n type PaymentPort,\n} from './checkout.js'\n\nexport interface CommerceOptions {\n /** The site's shop database (`@odoro-cli/cloud-connect`, or any `Base`). */\n readonly db: Base\n /** Where the money goes: Odoro's payment. */\n readonly payment: PaymentPort\n /** The secret Odoro signs its payment callbacks with. Empty = callbacks refused. */\n readonly callbackSecret: string\n /** The shop's currency. @defaultValue 'EUR' */\n readonly currency?: string\n /** Mark the cart and session cookies `Secure`. @defaultValue true */\n readonly secureCookie?: boolean\n /**\n * Where the sign-in links leave: Odoro sends them. Without it, the account\n * says it is not available — by name.\n */\n readonly mail?: MailPort\n}\n\nfunction refusal(\n kind: 'VALIDATION' | 'NOT_FOUND' | 'CONFLICT' | 'UNAVAILABLE',\n message: string,\n): ApiError {\n const extensions = { extensions: { erreur: message } }\n if (kind === 'NOT_FOUND') return new NotFoundError(message, extensions)\n if (kind === 'CONFLICT') return new ConflictError(message, extensions)\n if (kind === 'UNAVAILABLE')\n return new ServiceUnavailableError(message, undefined, extensions)\n return new ApiError('VALIDATION', message, extensions)\n}\n\n/** Domain refusals become contract errors; anything else stays an internal error. */\nasync function speaking<T>(work: () => Promise<T>): Promise<T> {\n try {\n return await work()\n } catch (cause) {\n if (cause instanceof CheckoutError) {\n throw refusal(\n cause.status === 404\n ? 'NOT_FOUND'\n : cause.status === 409\n ? 'CONFLICT'\n : 'VALIDATION',\n cause.message,\n )\n }\n if (cause instanceof CartError) throw refusal('CONFLICT', cause.message)\n throw cause\n }\n}\n\nconst text = z.string().max(200).optional()\nconst count = z.coerce.number().int().min(0).max(100_000).optional()\n\nexport function createCommerceModule(options: CommerceOptions) {\n const { db } = options\n const currency = options.currency ?? 'EUR'\n let installed: Promise<boolean> | null = null\n\n /** Once per process: the `shop` capability, at a version this module reads. */\n async function ready(): Promise<void> {\n installed ??= shopInstalled(db)\n if (!(await installed)) {\n installed = null\n throw refusal('UNAVAILABLE', \"La boutique de ce site n'est pas encore installée.\")\n }\n }\n\n async function cartOf(cookies: Cookies) {\n return await findCart(db, cookies.get(CART_COOKIE))\n }\n\n function keep(cookies: Cookies, token: string): void {\n cookies.set(CART_COOKIE, token, {\n httpOnly: true,\n sameSite: 'lax',\n maxAge: CART_MAX_AGE,\n ...(options.secureCookie === false ? { secure: false } : {}),\n })\n }\n\n const storefront = route({\n name: 'storefront.read',\n method: 'GET',\n path: '/api/storefront',\n auth: 'public',\n input: z.object({\n geste: z\n .enum(['catalogue', 'fiche', 'collections', 'commande', 'suggestions'])\n .default('catalogue'),\n produit: text,\n jeton: text,\n q: text,\n collection: text,\n genre: text,\n prix_min: count,\n prix_max: count,\n disponibles: z.string().optional(),\n tri: z\n .enum(['nouveautes', 'prix_croissant', 'prix_decroissant', 'nom'])\n .optional()\n .catch(undefined),\n combien: count,\n depuis: count,\n }),\n summary:\n 'The catalogue, a product page, the collections or an order, per the storefront contract.',\n handler: async ({ input }) => {\n await ready()\n if (input.geste === 'fiche') {\n const fiche = await readProduct(db, input.produit ?? '')\n if (fiche === null) throw refusal('NOT_FOUND', \"Cet article n'est plus en vente.\")\n return { fiche }\n }\n if (input.geste === 'collections') return { collections: await readCollections(db) }\n if (input.geste === 'commande') {\n const commande = await orderState(db, input.jeton ?? '')\n if (commande === null) throw refusal('NOT_FOUND', \"Cette commande n'existe pas.\")\n return { commande }\n }\n if (input.geste === 'suggestions') {\n const { produits } = await readCatalogue(db, { q: input.q ?? '', combien: 6 })\n return { suggestions: produits.map((p) => ({ id: p.id, nom: p.nom })) }\n }\n return await readCatalogue(db, {\n ...(input.q === undefined ? {} : { q: input.q }),\n ...(input.collection === undefined ? {} : { collection: input.collection }),\n ...(input.genre === undefined ? {} : { genre: input.genre }),\n ...(input.prix_min === undefined ? {} : { prixMin: input.prix_min }),\n ...(input.prix_max === undefined ? {} : { prixMax: input.prix_max }),\n disponibles: input.disponibles === '1',\n ...(input.tri === undefined ? {} : { tri: input.tri }),\n ...(input.combien === undefined ? {} : { combien: input.combien }),\n ...(input.depuis === undefined ? {} : { depuis: input.depuis }),\n })\n },\n })\n\n /** A page view. Nothing is counted or stored here: no tracker on the site. */\n const visit = route({\n name: 'storefront.visit',\n method: 'POST',\n path: '/api/storefront',\n auth: 'public',\n handler: () => ({ ok: true }),\n })\n\n const readCartRoute = route({\n name: 'storefront.cart.read',\n method: 'GET',\n path: '/api/storefront/cart',\n auth: 'public',\n handler: async ({ cookies }) => {\n await ready()\n const cart = await cartOf(cookies)\n if (cart === null) {\n return {\n panier: {\n panierId: '',\n jeton: '',\n courriel: null,\n lignes: [],\n combien: 0,\n sousTotalCentimes: 0,\n neuf: false,\n },\n }\n }\n return { panier: await readCart(db, cart) }\n },\n })\n\n const changeCart = route({\n name: 'storefront.cart.change',\n method: 'POST',\n path: '/api/storefront/cart',\n auth: 'public',\n input: z.object({\n geste: z.enum(['ajouter', 'quantite']).default('ajouter'),\n variante: z.string().regex(/^[0-9a-f-]{36}$/i, \"Cet article n'est plus en vente.\"),\n quantite: z.coerce.number().int().min(0).max(99).default(1),\n }),\n handler: async ({ input, cookies }) =>\n await speaking(async () => {\n await ready()\n const existing = await cartOf(cookies)\n const cart = existing ?? (await openCart(db))\n if (input.geste === 'quantite')\n await setQuantity(db, cart.id, input.variante, input.quantite)\n else await addToCart(db, cart.id, input.variante, input.quantite)\n // The cookie is set on a GESTURE, never on a visit.\n if (existing === null) keep(cookies, cart.token)\n return { panier: await readCart(db, cart, existing === null) }\n }),\n })\n\n const checkout = route({\n name: 'storefront.checkout',\n method: 'POST',\n path: '/api/storefront/checkout',\n auth: 'public',\n input: z.object({\n geste: z.enum(['ouvrir', 'chiffrer', 'payer']),\n caisse: z.string().max(64).optional(),\n code: z.string().max(40).optional(),\n courriel: text,\n nom: text,\n ligne1: text,\n ligne2: text,\n code_postal: z.string().max(20).optional(),\n ville: text,\n pays: z.string().max(40).optional(),\n telephone: z.string().max(30).optional(),\n }),\n handler: async ({ input, cookies }) =>\n await speaking(async () => {\n await ready()\n if (input.geste === 'ouvrir') {\n const cart = await cartOf(cookies)\n if (cart === null) throw new CheckoutError(400, 'Votre panier est vide.')\n const opened = await openCheckout(\n db,\n cart.id,\n {\n courriel: input.courriel ?? '',\n nom: input.nom ?? '',\n ligne1: input.ligne1 ?? '',\n ligne2: input.ligne2 ?? '',\n code_postal: input.code_postal ?? '',\n ville: input.ville ?? '',\n pays: input.pays ?? '',\n telephone: input.telephone ?? '',\n },\n currency,\n )\n return {\n caisse: opened.orderId,\n prixRevalorises: opened.repriced,\n total: await quote(db, opened.orderId),\n }\n }\n const orderId = input.caisse ?? ''\n if (!/^[0-9a-f-]{36}$/i.test(orderId))\n throw new CheckoutError(404, \"Cette caisse n'existe pas.\")\n if (input.geste === 'chiffrer')\n return { total: await applyCode(db, orderId, input.code) }\n await applyCode(db, orderId, input.code)\n return await pay(db, orderId, options.payment)\n }),\n })\n\n /** Odoro tells the payment's outcome. Signed: the browser never decides that an order is paid. */\n const callback = route({\n name: 'storefront.payment.callback',\n method: 'POST',\n path: '/api/storefront/payment-callback',\n auth: 'public',\n input: z.object({\n reference: z.string().min(1).max(200),\n etat: z.enum(['payee', 'echouee']),\n horodatage: z.coerce.number().int(),\n signature: z.string().min(1).max(128),\n }),\n handler: async ({ input }) => {\n await ready()\n if (!verifyCallback(options.callbackSecret, input)) {\n throw refusal('NOT_FOUND', 'Rappel de paiement refusé.')\n }\n const known = await confirmPayment(db, input.reference, input.etat)\n if (!known) throw refusal('NOT_FOUND', 'Paiement inconnu.')\n return { ok: true }\n },\n })\n\n // The account serves only where it can: a shop in 1.3.0, with a way to\n // send the link. Anywhere else it says it is not available — by name,\n // never a dead button.\n const accountRead = route({\n name: 'storefront.account.read',\n method: 'GET',\n path: '/api/storefront/account',\n auth: 'public',\n handler: async ({ cookies }) => {\n if (options.mail === undefined || !(await accountsInstalled(db)))\n return { connecte: false }\n const customer = await customerOf(db, cookies.get(CUSTOMER_COOKIE))\n if (customer === null) return { connecte: false }\n return { connecte: true, profil: await profile(db, customer) }\n },\n })\n const accountWrite = route({\n name: 'storefront.account.write',\n method: 'POST',\n path: '/api/storefront/account',\n auth: 'public',\n input: z.object({\n geste: z.enum(['lien', 'lettre', 'profil', 'deconnexion']).optional(),\n email: z.string().max(254).optional(),\n offres: z.boolean().optional(),\n nom: z.string().max(120).optional(),\n }),\n handler: async ({ input, cookies }) =>\n await speaking(async () => {\n const mail = options.mail\n if (mail === undefined || !(await accountsInstalled(db))) {\n throw refusal(\n 'UNAVAILABLE',\n \"Le compte client n'est pas encore disponible sur cette boutique.\",\n )\n }\n const geste = input.geste ?? 'lien'\n const who = {\n email: input.email,\n ...(input.nom === undefined ? {} : { name: input.nom }),\n ...(input.offres === undefined ? {} : { consent: input.offres }),\n }\n if (geste === 'lien') {\n await requestLink(db, mail, who)\n return { ok: true }\n }\n if (geste === 'lettre') {\n await subscribe(db, who)\n return { ok: true }\n }\n const session = cookies.get(CUSTOMER_COOKIE)\n if (geste === 'deconnexion') {\n await closeSession(db, session)\n cookies.clear(CUSTOMER_COOKIE)\n return { ok: true }\n }\n const customer = await customerOf(db, session)\n if (customer === null) {\n const message =\n 'Ce lien de connexion est périmé ou a déjà servi. Demandez-en un nouveau.'\n throw new ApiError('UNAUTHORIZED', message, { extensions: { erreur: message } })\n }\n await updateProfile(db, customer, {\n ...(input.nom === undefined ? {} : { name: input.nom }),\n ...(input.offres === undefined ? {} : { consent: input.offres }),\n })\n const after = (await customerOf(db, session)) ?? customer\n return { ok: true, profil: await profile(db, after) }\n }),\n })\n\n return defineModule({\n name: 'commerce',\n routes: [\n storefront,\n visit,\n readCartRoute,\n changeCart,\n checkout,\n callback,\n accountRead,\n accountWrite,\n ] as never,\n })\n}\n","/**\n * The payment, opened at Odoro.\n *\n * A site that sells never holds a payment provider's key: it asks Odoro to\n * open the payment, and Odoro tells it the outcome through the signed callback\n * (`/api/storefront/payment-callback`). The site's secret is its own — shared\n * with Odoro, with no other site — so a compromised site can open payments\n * for itself and nothing else.\n *\n * ## The request's signature\n *\n * `x-odoro-signature` is the hex HMAC-SHA256 of `${timestamp}.${body}`, and\n * `x-odoro-timestamp` the Unix time in seconds. Odoro refuses a signature more\n * than five minutes old: a request copied from a log cannot be replayed.\n *\n * @module\n */\n\nimport { createHmac } from 'node:crypto'\n\nimport { CheckoutError, type PaymentPort } from './checkout.js'\n\nexport interface OdoroPaymentOptions {\n /** Odoro's address, e.g. `https://odoro.ai`. */\n readonly origin: string\n /** This site's identifier at Odoro. */\n readonly site: string\n /** This site's secret, shared with Odoro alone. */\n readonly secret: string\n /** @internal For the tests. */\n readonly fetch?: typeof fetch\n /** @internal For the tests: seconds since the epoch. */\n readonly now?: () => number\n}\n\n/** Where Odoro opens a site's payment. */\nexport const ODORO_PAYMENT_PATH = '/api/payment/site-order'\n\nexport function signRequest(secret: string, timestamp: number, body: string): string {\n return createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex')\n}\n\nconst UNAVAILABLE = \"Le paiement n'a pas pu s'ouvrir. Réessayez dans un instant.\"\n\nexport function odoroPayment(options: OdoroPaymentOptions): PaymentPort {\n const send = options.fetch ?? fetch\n const now = options.now ?? (() => Math.floor(Date.now() / 1000))\n const address = new URL(ODORO_PAYMENT_PATH, options.origin).toString()\n\n return {\n open: async (order) => {\n if (options.secret === '') throw new CheckoutError(409, UNAVAILABLE)\n const body = JSON.stringify({\n site: options.site,\n commande: order.orderId,\n numero: order.number,\n totalCentimes: order.totalCents,\n devise: order.currency,\n courriel: order.email,\n })\n const timestamp = now()\n let reply: Response\n try {\n reply = await send(address, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-odoro-timestamp': String(timestamp),\n 'x-odoro-signature': signRequest(options.secret, timestamp, body),\n },\n body,\n signal: AbortSignal.timeout(15_000),\n })\n } catch {\n throw new CheckoutError(409, UNAVAILABLE)\n }\n const payload = (await reply.json().catch(() => ({}))) as {\n reference?: unknown\n adresse?: unknown\n erreur?: unknown\n }\n if (\n !reply.ok ||\n typeof payload.reference !== 'string' ||\n typeof payload.adresse !== 'string'\n ) {\n // Odoro's refusal is written for the buyer (\"the shop is not open yet\"):\n // it is passed on as it is. Anything else says to try again.\n const said =\n typeof payload.erreur === 'string' && reply.status < 500\n ? payload.erreur\n : UNAVAILABLE\n throw new CheckoutError(409, said)\n }\n return { reference: payload.reference, paymentUrl: payload.adresse }\n },\n }\n}\n","/**\n * The sign-in link, sent by Odoro.\n *\n * A shop holds no mail key: it asks Odoro to send the link, signed with the\n * site's secret exactly like a payment (`odoro-payment.ts`). Odoro writes the\n * address of the link itself — the shop's own published address — and only\n * takes the TOKEN from here: a compromised site cannot make Odoro mail a link\n * that leads anywhere else.\n *\n * @module\n */\n\nimport type { MailPort } from './account.js'\nimport { signRequest } from './odoro-payment.js'\n\nexport interface OdoroMailOptions {\n /** Odoro's address, e.g. `https://odoro.ai`. */\n readonly origin: string\n /** This site's identifier at Odoro. */\n readonly site: string\n /** This site's secret, shared with Odoro alone. */\n readonly secret: string\n /** @internal For the tests. */\n readonly fetch?: typeof fetch\n /** @internal For the tests: seconds since the epoch. */\n readonly now?: () => number\n}\n\n/** Where Odoro sends a site's sign-in link. */\nexport const ODORO_MAIL_PATH = '/api/hosting/site-login-link'\n\nexport function odoroMail(options: OdoroMailOptions): MailPort {\n const send = options.fetch ?? fetch\n const now = options.now ?? (() => Math.floor(Date.now() / 1000))\n const address = new URL(ODORO_MAIL_PATH, options.origin).toString()\n\n return {\n sendLoginLink: async ({ email, token }) => {\n if (options.secret === '') throw new Error('site secret missing')\n const body = JSON.stringify({ site: options.site, courriel: email, jeton: token })\n const timestamp = now()\n const reply = await send(address, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-odoro-timestamp': String(timestamp),\n 'x-odoro-signature': signRequest(options.secret, timestamp, body),\n },\n body,\n signal: AbortSignal.timeout(15_000),\n })\n if (!reply.ok) throw new Error(`Odoro refused the link (${reply.status})`)\n },\n }\n}\n","/**\n * A `Base` from a `pg` pool — without depending on `pg`.\n *\n * The shape is structural: any pool whose `query` and `connect` look like\n * node-postgres works, and the site keeps the driver version it chose.\n *\n * A transaction takes ONE client for its whole length: `BEGIN` on one\n * connection and `COMMIT` on another would commit nothing, silently.\n *\n * @module\n */\n\nimport type { Base, Query } from './base.js'\n\ninterface PgResult {\n rows: unknown[]\n}\n\nexport interface PgClientLike {\n query(text: string, values?: readonly unknown[]): Promise<PgResult>\n release(): void\n}\n\nexport interface PgPoolLike {\n query(text: string, values?: readonly unknown[]): Promise<PgResult>\n connect(): Promise<PgClientLike>\n}\n\nfunction reader(\n run: (text: string, values?: readonly unknown[]) => Promise<PgResult>,\n): Query {\n return {\n query: async <Row extends object>(text: string, values?: readonly unknown[]) => {\n const { rows } = await run(text, values)\n return { rows: rows as Row[] }\n },\n }\n}\n\nexport function baseFromPool(pool: PgPoolLike): Base {\n return {\n ...reader((text, values) => pool.query(text, values)),\n transaction: async (work) => {\n const client = await pool.connect()\n try {\n await client.query('BEGIN')\n const result = await work(reader((text, values) => client.query(text, values)))\n await client.query('COMMIT')\n return result\n } catch (cause) {\n await client.query('ROLLBACK').catch(() => undefined)\n throw cause\n } finally {\n client.release()\n }\n },\n }\n}\n"]}
|