@craftware/crafty-sdk 0.10.4 → 0.12.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.
@@ -117,6 +117,44 @@ An order can be read back with its token, which is in the order page link (`?t=`
117
117
  const order = await crafty.orders.get("CM-1001", token);
118
118
  ```
119
119
 
120
+ ## Discount coupons
121
+
122
+ The merchant creates codes in the panel (**Cupones**), with `upsert_coupon` or `POST /api/v1/coupons`. One coupon per order:
123
+
124
+ - Types: `percent` (with an optional `max_discount`), `fixed` (an amount) or `free_delivery`.
125
+ - A coupon applies to all products or to chosen ones (`applies_to: "entries"` plus product slugs).
126
+ - Optional limits: `min_subtotal` (the product subtotal before delivery), `max_uses`, `max_uses_per_email`, `starts_at` and `ends_at` (whole days in store time), and `is_active`.
127
+
128
+ The visitor types the code on the hosted checkout. It shows the discount, or the reason the code doesn't apply.
129
+
130
+ - The discount is spread over the lines before taxes.
131
+ - "Free delivery from" looks at the subtotal after the discount.
132
+ - A cancelled order gives its use back.
133
+ - An order with nothing left to pay is marked paid with `payment_method: "none"`.
134
+
135
+ To carry a code from a shared link (`https://monteverde.do/?cupon=BIENVENIDA10`) to the checkout:
136
+
137
+ ```ts
138
+ cart.captureCoupon(); // on page load: keeps ?cupon= (or ?coupon=) in the cart
139
+ const quote = await crafty.orders.quote(cart.items(), { coupon: cart.coupon() });
140
+ // quote.discount_total, quote.delivery_discount, quote.coupon, quote.coupon_error { reason, message, missing }
141
+ await crafty.checkout.start(cart.items(), { coupon: cart.coupon() });
142
+ ```
143
+
144
+ Show `quote.coupon_error.message` as it is: it is already in the visitor's language. Possible `reason` values:
145
+
146
+ - `not_found`
147
+ - `paused`
148
+ - `not_started`
149
+ - `expired`
150
+ - `exhausted`
151
+ - `used_by_email`
152
+ - `minimum` (with `missing`, the amount the cart still needs)
153
+ - `not_eligible`
154
+ - `too_many_attempts`
155
+
156
+ Failed codes are rate limited per visitor. The cart forgets the code once the order is placed. Orders, webhooks and `orders.get()` carry `discount_total`, `delivery_discount`, `coupon` and each line's `discount`.
157
+
120
158
  ## 5. Orders and notifications
121
159
 
122
160
  - Each order gets a number per environment (`<prefix>1001`, `1002`, …), status **Nuevo**, payment **Pendiente**.
@@ -144,8 +182,8 @@ No API key is required; the site is in the URL. Errors use the usual `{ error, m
144
182
  | Method | Path | Notes |
145
183
  | --- | --- | --- |
146
184
  | GET | `/api/v1/sites/{site}/store` | Public settings. |
147
- | POST | `/api/v1/sites/{site}/orders/quote` | Body `{ items: [{ slug, variant?, quantity }], fulfillment?: "delivery" \| "pickup", locale? }`. |
148
- | POST | `/api/v1/sites/{site}/checkout` | Body `{ items, return_url?, cancel_url?, fields?, fulfillment?, locale? }` → 201 `{ data: { id, url, expires_at } }`. |
185
+ | POST | `/api/v1/sites/{site}/orders/quote` | Body `{ items: [{ slug, variant?, quantity }], fulfillment?: "delivery" \| "pickup", locale?, coupon?, email? }`. |
186
+ | POST | `/api/v1/sites/{site}/checkout` | Body `{ items, return_url?, cancel_url?, fields?, fulfillment?, locale?, coupon? }` → 201 `{ data: { id, url, expires_at } }`. |
149
187
  | GET | `/api/v1/sites/{site}/orders/{number}` | Token in `?token=` or `X-Order-Token`. |
150
188
  | GET | `/api/v1/sites/{site}/checkout/{id}` | `{ data: { placed } }`: whether the session became an order; 404 once pruned. |
151
189
 
@@ -158,9 +196,12 @@ For an ERP, POS or backend: an OAuth token (the same as MCP) with `crafty:read`
158
196
  | Method | Path | Notes |
159
197
  | --- | --- | --- |
160
198
  | GET | `/api/v1/orders` | Filters `status`, `payment_status`, `metadata[erp_id]=…` (up to 5 pairs), `per_page` (max 100). |
161
- | POST | `/api/v1/orders` | Body like `create_order`: `customer`, `fulfillment`, `address?`, `payment_method`, `items`, `fields?`, `metadata?`, `locale?`, `expected_total?` → 201. |
199
+ | POST | `/api/v1/orders` | Body like `create_order`: `customer`, `fulfillment`, `address?`, `payment_method`, `items`, `fields?`, `metadata?`, `locale?`, `expected_total?`, `coupon?` → 201. |
162
200
  | GET | `/api/v1/orders/{number}` | The order with `payment_url` (the customer's order page), `fields`, `metadata`, `next_statuses` and `events`. |
163
- | PATCH | `/api/v1/orders/{number}` | Edit keys (`items` with `id` for existing lines, `quantity: 0` removes), plus `status`, `paid`, `payment_reference`, `internal_note`, `notify`. Returns the order and `applied`. |
201
+ | PATCH | `/api/v1/orders/{number}` | Edit keys (`items` with `id` for existing lines, `quantity: 0` removes, `coupon` to add, keep or remove a code with `null`, `coupon_below_minimum: "keep" \| "remove"`), plus `status`, `paid`, `payment_reference`, `internal_note`, `notify`. Returns the order and `applied`. |
202
+ | GET | `/api/v1/coupons` | The environment's coupons with `status`, `uses`, `orders`, `sales` and `discounted`. Editors need the `coupons` permission. |
203
+ | POST | `/api/v1/coupons` | Body like `upsert_coupon`: `code`, `type`, `value`, `max_discount?`, `min_subtotal?`, `applies_to?`, `products?`, `max_uses?`, `max_uses_per_email?`, `starts_at?`, `ends_at?`, `is_active?` → 201. |
204
+ | GET / PATCH / DELETE | `/api/v1/coupons/{code}` | Read, change (only the keys sent) or delete. A used coupon keeps its code and type, and DELETE pauses it instead. |
164
205
 
165
206
  Rejections use `{ error, message }` with the code (`PRICE_CHANGED` is 409 and includes the new `quote`).
166
207