@skuio/storefront 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mail.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { M as MailAdapter, a as MailMessage } from './types-CWreCyxc.js';
2
- import { w as StorefrontConfig, O as Order, F as Fulfillment } from './config-C4YCi7m4.js';
2
+ import { z as StorefrontConfig, O as Order, S as StoreQuote, F as Fulfillment } from './config-CCxTka_z.js';
3
3
  export { ConsoleMailOptions, consoleMail } from './mail/console.js';
4
4
  export { MailSendError, SendgridMailOptions, sendgridMail } from './mail/sendgrid.js';
5
5
  export { CloudflareMailOptions, CloudflareSendEmailBinding, cloudflareMail } from './mail/cloudflare.js';
@@ -31,5 +31,13 @@ declare function ownerPasswordResetMail(input: {
31
31
  expiresMinutes: number;
32
32
  }, ctx: MailContext): RenderedMail;
33
33
  declare function merchantNewOrderMail(order: Order, ctx: MailContext): RenderedMail;
34
+ /** Where a buyer opens a quote without signing in: the access key is the credential. */
35
+ declare function quoteUrl(quote: StoreQuote, siteUrl: string): string;
36
+ /** To the buyer: we have your request. Carries the link that is their way back in — no account needed. */
37
+ declare function quoteReceivedMail(quote: StoreQuote, ctx: MailContext): RenderedMail;
38
+ /** To the buyer: the quote is priced and waiting for their answer. */
39
+ declare function quoteReadyMail(quote: StoreQuote, ctx: MailContext): RenderedMail;
40
+ /** To the merchant: a new request, or the buyer's answer to an offer. */
41
+ declare function merchantQuoteMail(quote: StoreQuote, ctx: MailContext, event: 'requested' | 'accepted' | 'declined', orderNumber?: string | null): RenderedMail;
34
42
 
35
- export { type MailContext, type RenderedMail, escapeHtml, magicLinkMail, merchantNewOrderMail, orderConfirmationMail, ownerPasswordResetMail, partnerSignInMail, shippingConfirmationMail };
43
+ export { type MailContext, type RenderedMail, escapeHtml, magicLinkMail, merchantNewOrderMail, merchantQuoteMail, orderConfirmationMail, ownerPasswordResetMail, partnerSignInMail, quoteReadyMail, quoteReceivedMail, quoteUrl, shippingConfirmationMail };
package/dist/mail.js CHANGED
@@ -12,11 +12,15 @@ import {
12
12
  escapeHtml,
13
13
  magicLinkMail,
14
14
  merchantNewOrderMail,
15
+ merchantQuoteMail,
15
16
  orderConfirmationMail,
16
17
  ownerPasswordResetMail,
17
18
  partnerSignInMail,
19
+ quoteReadyMail,
20
+ quoteReceivedMail,
21
+ quoteUrl,
18
22
  shippingConfirmationMail
19
- } from "./chunk-UCREHLZH.js";
23
+ } from "./chunk-BD274TXU.js";
20
24
  import "./chunk-4PTKC5PH.js";
21
25
  export {
22
26
  MailSendError,
@@ -25,9 +29,13 @@ export {
25
29
  escapeHtml,
26
30
  magicLinkMail,
27
31
  merchantNewOrderMail,
32
+ merchantQuoteMail,
28
33
  orderConfirmationMail,
29
34
  ownerPasswordResetMail,
30
35
  partnerSignInMail,
36
+ quoteReadyMail,
37
+ quoteReceivedMail,
38
+ quoteUrl,
31
39
  sendgridMail,
32
40
  shippingConfirmationMail
33
41
  };
@@ -1,4 +1,4 @@
1
- import { O as Order } from '../config-C4YCi7m4.js';
1
+ import { O as Order } from '../config-CCxTka_z.js';
2
2
  import { PaymentAdapter, PaymentContext, PaymentEvent } from '../payments.js';
3
3
  import '../store.js';
4
4
  import '../driver-Cr4l6GgG.js';
@@ -1,6 +1,6 @@
1
1
  import { PaymentAdapter } from '../payments.js';
2
2
  import '../store.js';
3
- import '../config-C4YCi7m4.js';
3
+ import '../config-CCxTka_z.js';
4
4
  import '../driver-Cr4l6GgG.js';
5
5
  import '../types-CWreCyxc.js';
6
6
 
@@ -1,4 +1,4 @@
1
- import { O as Order } from '../config-C4YCi7m4.js';
1
+ import { O as Order } from '../config-CCxTka_z.js';
2
2
  import { PaymentContext, PaymentAdapter } from '../payments.js';
3
3
  import '../store.js';
4
4
  import '../driver-Cr4l6GgG.js';
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  escapeHtml
3
- } from "../chunk-UCREHLZH.js";
3
+ } from "../chunk-BD274TXU.js";
4
4
  import {
5
5
  formatMoney
6
6
  } from "../chunk-4PTKC5PH.js";
@@ -1,4 +1,4 @@
1
- import { A as Address, O as Order } from '../config-C4YCi7m4.js';
1
+ import { A as Address, O as Order } from '../config-CCxTka_z.js';
2
2
  import { Quote } from '../store.js';
3
3
  import { PaymentEvent, PaymentAdapter } from '../payments.js';
4
4
  import '../driver-Cr4l6GgG.js';
@@ -1,5 +1,5 @@
1
1
  import Stripe from 'stripe';
2
- import { O as Order } from '../config-C4YCi7m4.js';
2
+ import { O as Order } from '../config-CCxTka_z.js';
3
3
  import { PaymentContext, PaymentEvent, PaymentAdapter } from '../payments.js';
4
4
  import '../store.js';
5
5
  import '../driver-Cr4l6GgG.js';
@@ -1,5 +1,5 @@
1
1
  import { Store } from './store.js';
2
- import { O as Order, M as Money } from './config-C4YCi7m4.js';
2
+ import { O as Order, M as Money } from './config-CCxTka_z.js';
3
3
  import './driver-Cr4l6GgG.js';
4
4
  import './types-CWreCyxc.js';
5
5
 
package/dist/store.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { w as StorefrontConfig, C as Catalog, M as Money, a as Availability, j as OrderEvent, O as Order, m as OrderStatus, F as Fulfillment, u as ShippingOption, g as DiscountCode, Q as QuantityDiscount, A as Address, P as PartnerProfile, q as PublicDiscount, e as Customer } from './config-C4YCi7m4.js';
1
+ import { z as StorefrontConfig, C as Catalog, M as Money, a as Availability, j as OrderEvent, O as Order, m as OrderStatus, F as Fulfillment, x as ShippingOption, g as DiscountCode, r as QuantityDiscount, A as Address, P as PartnerProfile, q as PublicDiscount, e as Customer, S as StoreQuote } from './config-CCxTka_z.js';
2
2
  import { D as Database, a as DbStatement } from './driver-Cr4l6GgG.js';
3
3
  export { b as DbValue, c as bindable } from './driver-Cr4l6GgG.js';
4
4
  import { M as MailAdapter } from './types-CWreCyxc.js';
@@ -11,6 +11,8 @@ interface StoreEnv {
11
11
  SESSION_SECRET?: string;
12
12
  /** Merchant address that hears about every paid order. */
13
13
  ORDER_NOTIFICATION_EMAIL?: string;
14
+ /** Merchant address that hears about quote requests and answers; falls back to ORDER_NOTIFICATION_EMAIL. */
15
+ QUOTE_NOTIFICATION_EMAIL?: string;
14
16
  [key: string]: string | undefined;
15
17
  }
16
18
  interface StoreContext {
@@ -127,7 +129,7 @@ declare class CatalogService {
127
129
  private readonly ctx;
128
130
  private readonly ttlMs;
129
131
  constructor(ctx: StoreContext, ttlMs: number);
130
- /** The catalogue snapshot: active products with availability, and the category tree. */
132
+ /** The catalog snapshot: active products with availability, and the category tree. */
131
133
  load(): Promise<Catalog>;
132
134
  invalidate(): void;
133
135
  private build;
@@ -138,7 +140,7 @@ declare class CatalogService {
138
140
  /** Every product row including inactive ones — the Store API's view for `unknown_skus` checks. */
139
141
  existingSkus(skus: string[]): Promise<Set<string>>;
140
142
  /**
141
- * Products in keyset order, for an operations system importing the catalogue.
143
+ * Products in keyset order, for an operations system importing the catalog.
142
144
  * Ordered by `(updated_at, sku)` so a cursor is strictly monotonic and a
143
145
  * re-run from a watermark never repeats or skips a row.
144
146
  */
@@ -386,6 +388,32 @@ interface Quote {
386
388
  currency: string;
387
389
  option: ShippingOption;
388
390
  }
391
+ /** An order with every amount already decided, ready to insert. Amounts are minor units in `currency`. */
392
+ interface PricedOrderSpec {
393
+ email: string;
394
+ name: string | null;
395
+ phone: string | null;
396
+ currency: string;
397
+ lines: PricedLine[];
398
+ subtotal: number;
399
+ shipping: number;
400
+ tax: number;
401
+ discount: number;
402
+ total: number;
403
+ shippingAddress: Address;
404
+ billingAddress: Address | null;
405
+ shippingMethod: {
406
+ code: string;
407
+ name: string;
408
+ } | null;
409
+ notes: string | null;
410
+ metadata: Record<string, string>;
411
+ discountCode: string | null;
412
+ discountPartner: string | null;
413
+ commission: number;
414
+ /** Extra fields for the order's `created` event. */
415
+ createdEvent: Record<string, unknown>;
416
+ }
389
417
  declare class CheckoutService {
390
418
  private readonly ctx;
391
419
  private readonly catalog;
@@ -395,7 +423,7 @@ declare class CheckoutService {
395
423
  /** Shipping options a destination may choose from. */
396
424
  optionsForCountry(country: string): ShippingOption[];
397
425
  /**
398
- * Price a set of lines from the catalogue. Quantities are clamped to the
426
+ * Price a set of lines from the catalog. Quantities are clamped to the
399
427
  * store's per-line maximum; unknown or unavailable SKUs fail validation.
400
428
  */
401
429
  quote(items: CheckoutItem[], country: string, shippingOptionId: string, discountCode?: string | null): Promise<Quote>;
@@ -406,11 +434,22 @@ declare class CheckoutService {
406
434
  quoteDefault(items: CheckoutItem[], country: string, discountCode?: string | null): Promise<Quote>;
407
435
  /** Create a pending order the payment adapter can take from here. */
408
436
  createOrder(input: CheckoutInput): Promise<Order>;
437
+ /**
438
+ * Insert an order whose every amount is already decided. The checkout prices from the catalog first; an accepted
439
+ * store quote (QuotesService.accept) passes the offered prices. Nothing on the Store API reaches this: an order is
440
+ * only ever placed by a buyer.
441
+ */
442
+ placePricedOrder(spec: PricedOrderSpec): Promise<Order>;
409
443
  }
444
+ /**
445
+ * Clean a shipping address and report what it is missing into `details` (keys `shipping_address.*`). The country is
446
+ * upper-cased and the country's own format (state, postal code) is checked.
447
+ */
448
+ declare function checkShippingAddress(address: Address | null | undefined, details: Record<string, string>): Address;
410
449
 
411
450
  declare const CONNECT_CODE_MINUTES = 10;
412
451
  declare const API_KEY_PREFIX = "sk_store_";
413
- declare const CONNECT_SCOPES: readonly ["orders", "payments", "fulfillments", "inventory", "catalog"];
452
+ declare const CONNECT_SCOPES: readonly ["orders", "payments", "fulfillments", "inventory", "catalog", "quotes"];
414
453
  type ConnectScope = (typeof CONNECT_SCOPES)[number];
415
454
  declare const SCOPE_DESCRIPTIONS: Record<ConnectScope, string>;
416
455
  interface Connection {
@@ -684,6 +723,120 @@ declare class OwnersService {
684
723
  prune(): Promise<void>;
685
724
  }
686
725
 
726
+ interface QuoteRequestItem {
727
+ sku: string;
728
+ quantity: number;
729
+ }
730
+ /** What a buyer sends to ask for a quote. No account is needed: the email is the identity. */
731
+ interface QuoteRequestInput {
732
+ items: QuoteRequestItem[];
733
+ email: string;
734
+ name: string;
735
+ phone?: string | null;
736
+ company?: string | null;
737
+ /** Lead time, intended use, delivery constraints — whatever the buyer wants priced in. */
738
+ message?: string | null;
739
+ /** Optional at request time; required by the time the quote is accepted. */
740
+ shippingAddress?: Address | null;
741
+ metadata?: Record<string, string>;
742
+ }
743
+ /** The operations system's price for a quote. Replaces the lines: it may change quantities, drop or add SKUs. */
744
+ interface QuoteOfferInput {
745
+ lines: {
746
+ sku: string;
747
+ name?: string | null;
748
+ quantity: number;
749
+ unit_amount: Money;
750
+ }[];
751
+ shipping?: Money | null;
752
+ tax?: Money | null;
753
+ /** ISO-8601; the offer lapses after it. Null or absent: open-ended. */
754
+ expires_at?: string | null;
755
+ note?: string | null;
756
+ /** Email the buyer that the quote is ready (default true). */
757
+ notify_customer?: boolean;
758
+ }
759
+ interface QuoteAcceptInput {
760
+ /** Falls back to the address given with the request. One of the two must be complete. */
761
+ shippingAddress?: Address | null;
762
+ phone?: string | null;
763
+ /** The buyer's purchase-order number, kept on the order's notes and metadata. */
764
+ poNumber?: string | null;
765
+ notes?: string | null;
766
+ }
767
+ interface QuoteChangedPage {
768
+ quotes: StoreQuote[];
769
+ nextCursor: string | null;
770
+ }
771
+ /**
772
+ * Request for quote. A buyer asks for a price on products — signed in or as a guest who never makes an account —
773
+ * the operations system prices it over the Store API (`PATCH /quotes/{id}` with an `offer`), and the buyer accepts
774
+ * it, which places an order at exactly the offered prices, or declines it.
775
+ *
776
+ * The store never invents a price: a quote is either unpriced or carries the operations system's offer, and an
777
+ * order only comes from the buyer accepting one.
778
+ */
779
+ declare class QuotesService {
780
+ private readonly ctx;
781
+ private readonly catalog;
782
+ private readonly checkout;
783
+ private readonly orders;
784
+ constructor(ctx: StoreContext, catalog: CatalogService, checkout: CheckoutService, orders: OrdersService);
785
+ get(id: string): Promise<StoreQuote | null>;
786
+ require(id: string): Promise<StoreQuote>;
787
+ /** The emailed link's lookup: id plus the access key. This is how a guest gets back to their quote. */
788
+ getWithKey(id: string, key: string): Promise<StoreQuote | null>;
789
+ forCustomer(customerId: string): Promise<StoreQuote[]>;
790
+ getForCustomer(id: string, customerId: string): Promise<StoreQuote | null>;
791
+ count(): Promise<number>;
792
+ /** Quotes changed since a watermark, oldest first, keyset-paged on (updated_at, id); `updatedSince` is inclusive. */
793
+ changedSince(updatedSince: string | null, cursor: string | null, limit: number): Promise<QuoteChangedPage>;
794
+ events(id: string): Promise<{
795
+ id: number;
796
+ type: string;
797
+ data: Record<string, unknown> | null;
798
+ createdAt: string;
799
+ }[]>;
800
+ /**
801
+ * Record a request for quote. The buyer needs no account and no password: the customer row is keyed by email,
802
+ * exactly as a guest checkout's is, and the emailed access-key link is how they come back. Sold-out products may
803
+ * be quoted (lead time is part of the answer); unknown or withdrawn ones may not. Throttled per email address.
804
+ */
805
+ request(input: QuoteRequestInput): Promise<StoreQuote>;
806
+ /**
807
+ * Accept a priced quote: places an order at exactly the offered prices (lines, shipping, tax) and links it. The
808
+ * order is pay-later (`payment_provider: manual`, never expires) — the operations system invoices and marks it
809
+ * paid, as B2B terms usually go; a store that wants card payment can start an adapter on the returned order.
810
+ * A second accept returns the same order.
811
+ */
812
+ accept(id: string, input?: QuoteAcceptInput): Promise<{
813
+ quote: StoreQuote;
814
+ order: Order;
815
+ }>;
816
+ /** The buyer turns a quote down — before or after it is priced. */
817
+ decline(id: string, reason?: string | null): Promise<StoreQuote>;
818
+ /**
819
+ * Price a quote. The lines replace the request's: the operations system may change a quantity, drop a line or add
820
+ * an alternative (a SKU the store does not list needs a `name`). Re-offering a live or lapsed quote revises it.
821
+ */
822
+ offer(id: string, input: QuoteOfferInput): Promise<StoreQuote>;
823
+ setExternalReference(id: string, reference: string | null): Promise<StoreQuote>;
824
+ /** The merchant withdraws a quote. An accepted one has an order: cancel that instead. */
825
+ cancel(id: string, reason?: string | null): Promise<StoreQuote>;
826
+ /**
827
+ * Lapse offers past their `expires_at`. Lazy, like abandoned checkouts: it runs at the start of the Store API quote
828
+ * list and of an accept, so no cron is needed and a consumer never sees a lapsed offer as live.
829
+ */
830
+ expireStale(limit?: number): Promise<number>;
831
+ private assertNotFlooding;
832
+ private event;
833
+ private mailContext;
834
+ /** The merchant hears about requests and answers at QUOTE_NOTIFICATION_EMAIL, else ORDER_NOTIFICATION_EMAIL. */
835
+ private notifyMerchant;
836
+ private send;
837
+ private hydrate;
838
+ }
839
+
687
840
  declare const MAGIC_LINK_MINUTES = 15;
688
841
  declare const SESSION_DAYS = 30;
689
842
  interface MagicLink {
@@ -780,7 +933,7 @@ declare class RateLimiter {
780
933
  pruneStatement(): DbStatement;
781
934
  }
782
935
 
783
- declare const STORE_PLATFORM_VERSION = "2.1.0";
936
+ declare const STORE_PLATFORM_VERSION = "2.2.0";
784
937
  interface StoreOptions {
785
938
  db: Database;
786
939
  config: StorefrontConfig;
@@ -788,7 +941,7 @@ interface StoreOptions {
788
941
  mail?: MailAdapter;
789
942
  /** Injectable clock for tests. */
790
943
  clock?: () => Date;
791
- /** How long an isolate keeps a catalogue snapshot. 0 disables the cache. */
944
+ /** How long an isolate keeps a catalog snapshot. 0 disables the cache. */
792
945
  catalogCacheTtlMs?: number;
793
946
  log?: (message: string, data?: unknown) => void;
794
947
  }
@@ -803,6 +956,7 @@ interface StoreHealth {
803
956
  categories: number;
804
957
  orders: number;
805
958
  customers: number;
959
+ quotes: number;
806
960
  };
807
961
  /** True once an owner exists, so a consumer can tell the merchant to run `owner:create` first. */
808
962
  owner_configured: boolean;
@@ -820,6 +974,7 @@ interface Store {
820
974
  readonly customers: CustomersService;
821
975
  readonly orders: OrdersService;
822
976
  readonly checkout: CheckoutService;
977
+ readonly quotes: QuotesService;
823
978
  readonly sessions: SessionsService;
824
979
  readonly events: EventsService;
825
980
  readonly owners: OwnersService;
@@ -833,4 +988,4 @@ interface Store {
833
988
  */
834
989
  declare function createStore(options: StoreOptions): Store;
835
990
 
836
- export { ABANDONED_AFTER_MINUTES, API_KEY_PREFIX, type AuthorizeParams, type AuthorizeRequest, CONNECT_CODE_MINUTES, CONNECT_SCOPES, CatalogService, type CategoryInput, type CategoryListQuery, type CategoryPage, type CategoryRecord, type ChangedPage, type CheckoutInput, type CheckoutItem, CheckoutService, type ConnectScope, type Connection, ConnectionsService, CustomersService, Database, DbStatement, EventsService, type FulfillmentInput, type InventoryItemInput, type InventoryResult, InventoryService, type IssuedKey, MAGIC_LINK_MINUTES, type MagicLink, OWNER_RESET_MINUTES, OWNER_SESSION_DAYS, OrdersService, type Owner, type OwnerResetLink, type OwnerSession, OwnersService, PARTNER_LINK_MINUTES, PARTNER_SESSION_DAYS, PASSWORD_ITERATIONS, PASSWORD_MIN_LENGTH, type PartnerReport, type PartnerReportLine, type PartnerReportOptions, type PartnerTraffic, PartnersService, type PaymentRecord, type PricedLine, type ProductInput, type ProductListQuery, type ProductPage, type ProductRecord, type Quote, type RateLimitPolicy, RateLimiter, SCOPE_DESCRIPTIONS, SESSION_DAYS, STORE_PLATFORM_VERSION, type Session, SessionsService, type Store, type StoreContext, type StoreEnv, StoreError, type StoreErrorCode, type StoreHealth, type StoreOptions, type TrafficDay, type TrafficSource, type VisitInput, createStore, deriveAvailability, hashPassword, hashToken, isLocalHost, newId, normalizeEmail, normalizeOwnerEmail, passwordProblem, randomBase64Url, randomToken, secretsEqual, sha256Hex, verifyPassword };
991
+ export { ABANDONED_AFTER_MINUTES, API_KEY_PREFIX, type AuthorizeParams, type AuthorizeRequest, CONNECT_CODE_MINUTES, CONNECT_SCOPES, CatalogService, type CategoryInput, type CategoryListQuery, type CategoryPage, type CategoryRecord, type ChangedPage, type CheckoutInput, type CheckoutItem, CheckoutService, type ConnectScope, type Connection, ConnectionsService, CustomersService, Database, DbStatement, EventsService, type FulfillmentInput, type InventoryItemInput, type InventoryResult, InventoryService, type IssuedKey, MAGIC_LINK_MINUTES, type MagicLink, OWNER_RESET_MINUTES, OWNER_SESSION_DAYS, OrdersService, type Owner, type OwnerResetLink, type OwnerSession, OwnersService, PARTNER_LINK_MINUTES, PARTNER_SESSION_DAYS, PASSWORD_ITERATIONS, PASSWORD_MIN_LENGTH, type PartnerReport, type PartnerReportLine, type PartnerReportOptions, type PartnerTraffic, PartnersService, type PaymentRecord, type PricedLine, type PricedOrderSpec, type ProductInput, type ProductListQuery, type ProductPage, type ProductRecord, type Quote, type QuoteAcceptInput, type QuoteChangedPage, type QuoteOfferInput, type QuoteRequestInput, type QuoteRequestItem, QuotesService, type RateLimitPolicy, RateLimiter, SCOPE_DESCRIPTIONS, SESSION_DAYS, STORE_PLATFORM_VERSION, type Session, SessionsService, type Store, type StoreContext, type StoreEnv, StoreError, type StoreErrorCode, type StoreHealth, type StoreOptions, type TrafficDay, type TrafficSource, type VisitInput, checkShippingAddress, createStore, deriveAvailability, hashPassword, hashToken, isLocalHost, newId, normalizeEmail, normalizeOwnerEmail, passwordProblem, randomBase64Url, randomToken, secretsEqual, sha256Hex, verifyPassword };