types/api.tstypescript
/**
 * API type definitions based on NextCommerce Campaigns API schema
 */

// Re-export from the OpenAPI spec we reviewed
export interface Campaign {
  currency: string;
  language: string;
  name: string;
  packages: PackageSerializer[];
  payment_env_key: string;
  shipping_methods: ShippingOption[];
  available_express_payment_methods?: PaymentMethodOption[];
  available_payment_methods?: PaymentMethodOption[];
  available_currencies?: Array<{ code: string; label: string }>;
  available_shipping_countries?: Array<{ code: string; label: string }>;
}

export interface PackageSerializer {
  ref_id: number;
  external_id: number;
  name: string;
  price: string;
  price_total: string;
  price_retail?: string;
  price_retail_total?: string;
  price_recurring?: string;
  price_recurring_total?: string;
  qty: number;
  image: string;
  is_recurring: boolean;
  interval?: 'day' | 'month' | null;
  interval_count?: number | null;
}

export interface Cart {
  checkout_url: string;
  currency: string;
  lines: CartLine[];
  total_excl_tax: string;
  total_incl_tax: string;
  total_excl_tax_excl_discounts: string;
  total_incl_tax_excl_discounts: string;
  total_discounts: string;
  discounts: Voucher[];
  user: User;
  attribution?: MarketingAttribution;
}

export interface Discount {
  offer_id: number;
  amount: string;
  description?: string;
  name?: string;
  percentage?: string;
}

export interface SummaryLine {
  package_id: number;
  quantity: number;
  discounts: Discount[];
  original_unit_price: string;
  original_package_price: string;
  unit_price: string;
  package_price: string;
  subtotal: string;
  total_discount: string;
  total: string;
  // Package data enriched from campaign — populated by the cart store after the API call
  name?: string;
  image?: string;
  qty?: number;
  price?: string;
  price_total?: string;
  price_retail?: string;
  price_retail_total?: string;
  price_recurring?: string;
  price_recurring_total?: string;
  is_recurring?: boolean;
  interval?: 'day' | 'month' | null;
  interval_count?: number | null;
  original_recurring_price?: string;
  currency?: string;
  product_name?: string;
  product_variant_name?: string;
  product_sku?: string | null;
  product_variant_attribute_values?: Array<{
    code: string;
    name: string;
    value: string;
  }>;
  /** Custom key-value properties — enriched from cart item after calculate. */
  properties?: Record<string, string>;
}

export interface ShippingMethodSummary {
  id: number;
  name: string;
  code: string;
  original_price: string;
  price: string;
  discounts: Discount[];
}

export interface CartSummary {
  lines: SummaryLine[];
  shipping_method: ShippingMethodSummary;
  offer_discounts: Discount[];
  voucher_discounts: Discount[];
  subtotal: string;
  total_discount: string;
  total: string;
  currency: string;
}

export interface CartLine {
  id: number;
  quantity: number;
  price_excl_tax: string;
  price_incl_tax: string;
  price_excl_tax_excl_discounts: string;
  price_incl_tax_excl_discounts: string;
  product_title: string;
  product_sku: string;
  image: string;
  is_upsell: boolean;
}

/**
 * A placed order, exactly as the orders API returns it — its lines, totals,
 * shipping, addresses, and the URLs the shopper is sent to next.
 *
 * This is **the order object the SDK holds**: creating an order, fetching one by
 * `ref_id`, and adding a post-purchase upsell all resolve to it, and it is what
 * the order store ({@link useOrderStore}) keeps on a receipt or upsell page.
 * Read it when you need any total, line, or address.
 *
 * {@link OrderData} is a smaller, older view of this same object — six fields
 * plus loosely typed `lines`/`user` — and is only the declared payload type of
 * the `order:completed` event. At runtime that event delivers a full `Order`.
 *
 * Every money field is a decimal **string** in the order's `currency` (`"59.98"`),
 * not a number, so it survives JSON without rounding. Parse it before doing
 * arithmetic.
 *
 * @example
 * ```ts
 * import { useOrderStore, type Order } from '@next-commerce/campaign-cart';
 *
 * const order: Order | null = useOrderStore.getState().order;
 * if (order) {
 *   console.log(order.number, order.total_incl_tax); // "NX-10428" "59.98"
 *   const upsells = order.lines.filter(line => line.is_upsell);
 *   console.log(`${upsells.length} upsell line(s) added after checkout`);
 * }
 * ```
 * @category Orders
 */
export interface Order {
  /**
   * The order's reference. It is the id everything else keys off: the SDK reads
   * it from `?ref_id=` to load the order on a receipt or upsell page, and sends
   * it back to add post-purchase upsells.
   */
  ref_id: string;
  /** Human-facing order number to show the customer, e.g. `"NX-10428"`. */
  number: string;
  /** ISO currency code the order was charged in, e.g. `"USD"`. */
  currency: string;
  /**
   * Every line on the order, one per package. Lines added after checkout by an
   * upsell carry `is_upsell: true`, which is how a thank-you page tells the
   * original purchase apart from what was added later.
   */
  lines: OrderLine[];
  /** Order grand total before tax. */
  total_excl_tax: string;
  /** Order grand total the customer was charged, tax included. */
  total_incl_tax: string;
  /** Tax charged on the order. */
  total_tax: string;
  /** Everything discounted off the order — offer discounts and vouchers together. */
  total_discounts: string;
  /** Shipping charged before tax. */
  shipping_excl_tax: string;
  /** Shipping charged, tax included. */
  shipping_incl_tax: string;
  /** Tax charged on shipping. */
  shipping_tax: string;
  /** Display name of the shipping method chosen at checkout, e.g. `"Standard"`. */
  shipping_method: string;
  /** Code of that shipping method, matching the campaign's `shipping_methods[].code`. */
  shipping_code: string;
  /**
   * Tax presentation hint the orders endpoint returns for stores that show tax
   * as a separate line. The SDK does not read it; it is here because the API
   * sends it. Absent for stores that do not display tax separately.
   */
  display_taxes?: string;
  /**
   * The discounts applied to the order, itemised. Each entry carries `amount`
   * plus, when the API knows them, `offer_id`, `name`, `description`, and
   * `percentage`. Use {@link Order.total_discounts | total_discounts} for the
   * single "you saved" figure.
   */
  discounts: Discount[];
  /** The customer the order was placed for — name, email, and phone. */
  user: OrderUser;
  /** Where the goods ship. Absent on orders with nothing to ship. */
  shipping_address?: OrderAddress;
  /** Where the card is billed. Absent when it is the same as the shipping address. */
  billing_address?: OrderAddress;
  /**
   * The marketing attribution captured with the order — UTM parameters, click
   * ids, affiliate and funnel. Absent when the visit carried none.
   */
  attribution?: MarketingAttribution;
  /**
   * How the order was paid for, as the API's own code — `"paypal"`,
   * `"card_token"`, `"apple_pay"` and so on. The orders API records the method
   * chosen at checkout and returns it on every order it sends back, so a
   * receipt page can name it.
   *
   * `null` on an order the platform has no method recorded for. There is no
   * "assume card" default: a page that shows this shows nothing when it is
   * null, rather than naming a method the shopper did not use.
   *
   * `data-next-display="order.paymentMethod"` renders this through a friendlier
   * label for the common methods — see the order-display guide's
   * [display paths](../features/display/order-display/guide/reference/display-paths.md).
   *
   * Two of {@link PaymentMethod}'s values only ever arrive here and can never be
   * sent: `external`, a payment taken outside the platform, and `saved_card`, a
   * card already on file. Both are recorded in the admin dashboard.
   */
  payment_method?: PaymentMethod | null;
  /** URL of the hosted order-status/receipt page for this order. */
  order_status_url: string;
  /**
   * Where to send the shopper to finish paying, for payment methods that need a
   * further step (PayPal, Klarna, and other redirect flows). When present the
   * SDK redirects here instead of to
   * {@link Order.order_status_url | order_status_url}.
   */
  payment_complete_url?: string;
  /**
   * Whether lines may still be added to this order. `false` means the order is
   * closed to upsells, and the SDK hides every offer on an upsell page rather
   * than letting a shopper accept one that will fail.
   */
  supports_post_purchase_upsells: boolean;
  /** `true` for a test-mode order — a real order record, but no money moved. */
  is_test: boolean;
  /**
   * What shows up next to the charge on the customer's card or bank statement.
   * Up to 20 characters — letters, numbers, spaces, and `.`, `-`, `*`.
   *
   * `null` or absent when the store has not configured one; the gateway's own
   * default descriptor applies instead. The SDK does not read this field today —
   * declared because the orders API sends it on every order.
   */
  statement_descriptor?: string | null;
}

/**
 * One line on a placed {@link Order} — a package the customer bought, at the
 * price they were charged for it.
 *
 * Money fields are decimal strings in the order's currency. The
 * `*_excl_discounts` pair is the price *before* discounts, so
 * `price_incl_tax_excl_discounts - price_incl_tax` is what this line saved.
 * @category Orders
 */
export interface OrderLine {
  /** The line's id on the order. Use it to tell two lines of the same package apart. */
  id: number;
  /** Product image URL for the line, for showing on a receipt. */
  image: string;
  /**
   * `true` when this line was added after checkout by a post-purchase upsell,
   * `false` for the original purchase.
   */
  is_upsell: boolean;
  /** What this line was charged, before tax and after discounts. */
  price_excl_tax: string;
  /** What this line would have cost before tax with no discounts applied. */
  price_excl_tax_excl_discounts: string;
  /** What this line was charged, tax included and after discounts. */
  price_incl_tax: string;
  /** What this line would have cost with tax and no discounts applied. */
  price_incl_tax_excl_discounts: string;
  /** SKU of the purchased variant. */
  product_sku: string;
  /** Product name to display on the line. */
  product_title: string;
  /** Longer product description, when the catalog has one. */
  product_description?: string;
  /** The variant the customer chose (e.g. `"Large / Blue"`), when the product has variants. */
  variant_title?: string;
  /** Units bought on this line. */
  quantity: number;
  /**
   * Catalog id of the product this line belongs to. `null` when the API has no
   * catalog product for the line. The SDK does not read this field today —
   * declared because the orders API sends it on every line.
   */
  product_id?: number | null;
  /**
   * Catalog id of the specific variant purchased (size, color, and similar
   * choices). `null` when the product has no variants, or the line predates
   * variant tracking. The SDK does not read this field today — declared because
   * the orders API sends it on every line.
   */
  variant_id?: number | null;
  /**
   * Arbitrary key-value data attached to the line at checkout — internal keys
   * the platform uses for its own bookkeeping, filtered out before this response
   * is sent. `null` or absent when nothing was attached. The SDK does not read
   * this field today — declared because the orders API sends it on every line.
   */
  metadata?: Record<string, unknown> | null;
  /**
   * Customer-facing custom values collected for this line at checkout — a gift
   * note or an engraving, for example — as an ordered list rather than the
   * {@link OrderLine.metadata | metadata} dictionary. `null` or empty when none
   * were collected. The SDK does not read this field today — declared because
   * the orders API sends it on every line.
   */
  properties?: OrderLineProperty[] | null;
}

/**
 * One customer-facing custom value collected on an {@link OrderLine} at
 * checkout — a gift message, an engraving, a personalization choice.
 * @category Orders
 */
export interface OrderLineProperty {
  /** The field's name, e.g. `"engraving"`. */
  key: string;
  /** What the customer entered or chose for that field. */
  value: string;
}

/**
 * Where a visitor came from, captured on the cart and carried onto the
 * {@link Order} — the UTM parameters, click ids, affiliate and funnel of the
 * visit that converted.
 *
 * Every field is optional: a direct visit with no campaign parameters produces
 * an object with almost nothing in it, and absent means "not present on the
 * landing URL", never "attribution failed".
 * @category Attribution
 */
export interface MarketingAttribution {
  /** `utm_source` — which site or channel sent the visit. */
  utm_source?: string;
  /** `utm_medium` — the kind of traffic, e.g. `"cpc"`, `"email"`. */
  utm_medium?: string;
  /** `utm_campaign` — the campaign name the ad or email belonged to. */
  utm_campaign?: string;
  /** `utm_term` — the paid keyword, when the source supplies one. */
  utm_term?: string;
  /** `utm_content` — which creative or link variant was clicked. */
  utm_content?: string;
  /** Google click id, for matching the order back to a Google Ads click. */
  gclid?: string;
  /** Affiliate identifier credited with the sale. */
  affiliate?: string;
  /** Funnel identifier for the page flow the visitor went through. */
  funnel?: string;
  /** First affiliate sub-id, as passed on the landing URL. */
  subaffiliate1?: string;
  /** Second affiliate sub-id. */
  subaffiliate2?: string;
  /** Third affiliate sub-id. */
  subaffiliate3?: string;
  /** Fourth affiliate sub-id. */
  subaffiliate4?: string;
  /** Fifth affiliate sub-id. */
  subaffiliate5?: string;
  /**
   * Everything else the SDK recorded about the visit — landing page, referrer,
   * device, timestamp, and any custom values the page added.
   */
  metadata?: Record<string, any>;
}

export interface ShippingOption {
  ref_id: number;
  code: string;
  price: string;
}

export interface PaymentMethodOption {
  code: string;
  label: string;
}

export interface User {
  accepts_marketing?: boolean;
  email?: string;
  first_name: string;
  ip?: string;
  language: string;
  last_name: string;
  phone_number?: string;
  user_agent?: string;
}

/**
 * The customer a placed {@link Order} belongs to, as the orders API returns it.
 *
 * Same fields as the customer on a cart; it is a separate name because the two
 * come from different endpoints and may diverge.
 * @category Orders
 */
export interface OrderUser {
  /** Whether the customer opted in to marketing email at checkout. */
  accepts_marketing?: boolean;
  /** Email the receipt was sent to. Absent on orders placed without one. */
  email?: string;
  /** Customer's first name. */
  first_name: string;
  /** IP address the order was placed from, when the API returns it. */
  ip?: string;
  /** Language code the customer checked out in, e.g. `"en"`. */
  language: string;
  /** Customer's last name. */
  last_name: string;
  /** Phone number given at checkout, when one was collected. */
  phone_number?: string;
  /** Browser user-agent string recorded with the order, when the API returns it. */
  user_agent?: string;
}

/**
 * A postal address on a placed {@link Order} — where it ships, or where the card
 * is billed.
 *
 * The `line1`–`line4` shape comes from the API: `line4` is the **city**, and
 * `state` and `postcode` are optional because not every country has them. An
 * absent field means the country does not use it or the shopper left it blank,
 * so print the lines that are present rather than assuming a fixed layout.
 * @category Orders
 */
export interface OrderAddress {
  /** Two-letter country code, e.g. `"US"`. */
  country: string;
  /** Recipient's first name. */
  first_name: string;
  /** Recipient's last name. */
  last_name: string;
  /** Street address. */
  line1: string;
  /** Apartment, suite, or unit. */
  line2?: string;
  /** Third address line, for addresses that need one. */
  line3?: string;
  /** City. */
  line4: string;
  /** Delivery notes the shopper left for the courier. */
  notes?: string;
  /** Contact phone for the delivery. */
  phone_number?: string;
  /** Postal or ZIP code. Absent for countries that do not use one. */
  postcode?: string;
  /** State, province, or region. Absent for countries that do not use one. */
  state?: string;
}

export interface Voucher {
  amount: string;
  description?: string;
  name?: string;
}

/**
 * How an order is paid for — the code the orders API accepts on
 * `payment_detail.payment_method` and reports back on
 * {@link Order.payment_method | Order.payment_method}.
 *
 * `card_token` is a card, and it is a **token** rather than a number: the card
 * itself is entered in the payment provider's own hosted fields and never
 * reaches this page, this SDK, or this request. There is no plain "credit card"
 * code here. A page writes `data-next-payment-method="credit_card"` and the
 * checkout store calls it `credit-card`, but what goes on the order is only ever
 * the token. The rest name the wallet or scheme the shopper used.
 *
 * Every one of them except a directly-charged card sends the shopper away to pay
 * and brings them back (a card only when the bank asks for 3-D Secure), so an
 * order created with any of these can come back carrying
 * {@link Order.payment_complete_url}.
 *
 * **Three of these are read-only.** `external` (a payment taken outside the
 * platform), `saved_card` (a card already on file) and any method the merchant
 * has not enabled arrive on a fetched order but are never something the SDK
 * sends: it has no flow that produces them.
 *
 * SEPA Direct Debit is `sepa_debit`, the name the orders API field lists. The
 * platform's payment-methods guide calls the same method `sepa_direct`, which is
 * not a value here.
 * @category Orders
 */
export type PaymentMethod =
  | 'apple_pay'
  | 'card_token'
  | 'saved_card'
  | 'paypal'
  | 'klarna'
  | 'ideal'
  | 'bancontact'
  | 'giropay'
  | 'google_pay'
  | 'sofort'
  | 'sepa_debit'
  | 'swish'
  | 'twint'
  | 'link'
  | 'affirm'
  | 'external';

// Request/Response types
export interface CartBase {
  address?: AddressCart;
  attribution?: Attribution;
  currency?: string;
  lines: LineWithUpsell[];
  user: UserCreateCart;
  vouchers?: string[];
}

export interface AddressCart {
  country: string;
  first_name: string;
  last_name: string;
  line1: string;
  line2?: string;
  line3?: string;
  line4: string; // City
  notes?: string;
  phone_number?: string;
  postcode?: string;
  state?: string;
}

export interface Attribution {
  affiliate?: string;
  funnel?: string;
  gclid?: string;
  metadata?: Record<string, any>;
  subaffiliate1?: string;
  subaffiliate2?: string;
  subaffiliate3?: string;
  subaffiliate4?: string;
  subaffiliate5?: string;
  utm_campaign?: string;
  utm_content?: string;
  utm_medium?: string;
  utm_source?: string;
  utm_term?: string;
  everflow_transaction_id?: string;
}

export interface LineWithUpsell {
  is_upsell?: boolean;
  package_id: number;
  quantity: number;
  properties?: Record<string, string>;
}

export interface UserCreateCart {
  accepts_marketing?: boolean;
  email?: string;
  first_name: string;
  language: string;
  last_name: string;
  phone_number?: string;
}

export interface CreateOrder {
  attribution?: Attribution;
  billing_address?: Address;
  billing_same_as_shipping_address?: boolean;
  currency?: string;
  lines: LineWithUpsell[];
  payment_detail: Payment;
  payment_failed_url?: string;
  shipping_address?: Address;
  shipping_method: number;
  success_url: string;
  use_default_billing_address?: boolean;
  use_default_shipping_address?: boolean;
  user?: OrderUser;
  vouchers?: string[];
}

export interface CartCalculateSummary {
  lines: LineWithUpsell[];
  vouchers?: string[];
  currency?: string | null;
  shipping_method?: number;
}

export interface Address {
  country: string;
  first_name: string;
  is_default_for_billing?: boolean;
  is_default_for_shipping?: boolean;
  last_name: string;
  line1: string;
  line2?: string;
  line3?: string;
  line4: string; // City
  notes?: string;
  phone_number?: string;
  postcode?: string;
  state?: string;
}

export interface Payment {
  card_token?: string;
  external_payment_method?: string;
  payment_gateway?: number;
  payment_gateway_group?: number;
  /**
   * How to charge for this order.
   *
   * Wider than {@link PaymentMethod} on purpose: a page may offer a method this
   * SDK release has never heard of, and the SDK sends that name through rather
   * than substituting a card for it. The API is the authority on what it accepts
   * request. It either creates the order (and answers with a
   * {@link Order.payment_complete_url} to finish paying at) or rejects the
   * request. `Order.payment_method` on the way back stays the known list.
   */
  payment_method: PaymentMethod | (string & {});
}

export interface AddUpsellLine {
  lines: UpsellLineItem[];
  payment_detail?: PaymentDetail;
  currency?: string;
  vouchers?: string[];
}

export interface UpsellLineItem {
  package_id: number;
  quantity: number;
  properties?: Record<string, string>;
}

export interface PaymentDetail {
  payment_gateway?: number;
  payment_gateway_group?: number;
}

export interface AddressAutocompleteResult {
  label: string;
  address: {
    line1: string;
    line2?: string;
    line3?: string;
    city: string;
    state: string;
    state_code: string;
    postcode: string;
    country: string;
    country_code: string;
  };
}

export interface AddressAutocomplete {
  results: AddressAutocompleteResult[];
}