@kizlo/woocommerce 0.1.9 → 0.2.1

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/index.d.ts CHANGED
@@ -1329,7 +1329,7 @@ declare function woocommerce(): kizlo0.Extension<"woocommerce", {
1329
1329
  attributes: {
1330
1330
  id: number;
1331
1331
  name: string;
1332
- taxonomy: string;
1332
+ taxonomy: string | null;
1333
1333
  hasVariations: boolean;
1334
1334
  terms: {
1335
1335
  id: number;
@@ -1537,7 +1537,7 @@ declare function woocommerce(): kizlo0.Extension<"woocommerce", {
1537
1537
  attributes: {
1538
1538
  id: number;
1539
1539
  name: string;
1540
- taxonomy: string;
1540
+ taxonomy: string | null;
1541
1541
  hasVariations: boolean;
1542
1542
  terms: {
1543
1543
  id: number;
@@ -1719,7 +1719,7 @@ declare function woocommerce(): kizlo0.Extension<"woocommerce", {
1719
1719
  taxonomy: string;
1720
1720
  description: string;
1721
1721
  count: number;
1722
- type: "text" | "color" | "image";
1722
+ type: "image" | "text" | "color";
1723
1723
  swatch: string | null;
1724
1724
  }[];
1725
1725
  taxonomyTerms: {
@@ -2424,8 +2424,7 @@ declare function woocommerce(): kizlo0.Extension<"woocommerce", {
2424
2424
  retry: kizlo0.Procedure<"api", {
2425
2425
  body: {
2426
2426
  key: string;
2427
- paymentMethod: string;
2428
- billingAddress?: {
2427
+ billingAddress: {
2429
2428
  firstName: string;
2430
2429
  lastName: string;
2431
2430
  phone: string;
@@ -2437,7 +2436,8 @@ declare function woocommerce(): kizlo0.Extension<"woocommerce", {
2437
2436
  email: string;
2438
2437
  company?: string | undefined;
2439
2438
  address2?: string | undefined;
2440
- } | undefined;
2439
+ };
2440
+ paymentMethod: string;
2441
2441
  shippingAddress?: {
2442
2442
  firstName: string;
2443
2443
  lastName: string;
package/dist/index.js CHANGED
@@ -1,10 +1,15 @@
1
- import { CurrencyFormat, IdentifierInput, KizloError, ListMetadata, Seo, WC_CORE_BASE, WC_STORE_BASE, WP_KIZLO_BASE, createExtension, createMiddleware, createProcedure, defineErrorMap, deserializeCurrencyFormat, deserializeListMetadata } from "kizlo";
1
+ import { CurrencyFormat, IdentifierInput, KizloError, ListMetadata, Seo, createExtension, createMiddleware, createProcedure, defineErrorMap, deserializeCurrencyFormat, deserializeListMetadata } from "kizlo";
2
2
  import { BooleanLike, Media, Metadata, NumberLike, arrayable, normalizeArrayableValue, random, seconds, timestampSec, toPublicMetadata, tryCatch } from "@kizlo/shared";
3
3
  import { SignJWT, jwtVerify } from "jose";
4
4
  import z$1, { z } from "zod/v4";
5
5
  import z$2 from "zod";
6
6
 
7
7
  //#region src/session.ts
8
+ const GUEST_COOKIE_OPTIONS = {
9
+ httpOnly: true,
10
+ path: "/",
11
+ sameSite: "lax"
12
+ };
8
13
  async function mintGuestToken(secret, ttlSeconds) {
9
14
  const sub = random({
10
15
  length: 32,
@@ -49,10 +54,7 @@ function sessionMiddleware(options) {
49
54
  await context.cookies.set({
50
55
  name: cookieName,
51
56
  value: jwt,
52
- options: {
53
- httpOnly: true,
54
- sameSite: "lax"
55
- }
57
+ options: GUEST_COOKIE_OPTIONS
56
58
  });
57
59
  return next({ context: { sessionHeaders: getCartHeaders({
58
60
  token: sub,
@@ -65,10 +67,7 @@ function sessionMiddleware(options) {
65
67
  await context.cookies.set({
66
68
  name: cookieName,
67
69
  value: jwt,
68
- options: {
69
- httpOnly: true,
70
- sameSite: "lax"
71
- }
70
+ options: GUEST_COOKIE_OPTIONS
72
71
  });
73
72
  throw new KizloError("CART_SESSION_EXPIRED");
74
73
  }
@@ -80,17 +79,14 @@ function sessionMiddleware(options) {
80
79
  if (foundToken) {
81
80
  const [err, data] = await tryCatch(verifyToken(foundToken, context.config.siteSecret));
82
81
  if (!err) {
83
- const response = await context.wordpress.post("/cart/merge", {
84
- base: WP_KIZLO_BASE,
85
- headers: getCartHeaders({
86
- userId: auth.id,
87
- token: data.sub,
88
- connInfo
89
- })
90
- });
82
+ const response = await context.wordpress.woocommerce.kizlo.cart.merge({}, { headers: getCartHeaders({
83
+ userId: auth.id,
84
+ token: data.sub,
85
+ connInfo
86
+ }) });
91
87
  if (response.error) context.logger.error("CART_MERGE_FAILED", response.error);
92
88
  }
93
- await context.cookies.delete(cookieName);
89
+ await context.cookies.delete(cookieName, GUEST_COOKIE_OPTIONS);
94
90
  }
95
91
  return next({ context: { sessionHeaders: getCartHeaders({
96
92
  userId: auth.id,
@@ -228,7 +224,7 @@ const ProductAttributeTermRef = z.object({
228
224
  const ProductAttributeRef = z.object({
229
225
  id: z.number(),
230
226
  name: z.string(),
231
- taxonomy: z.string(),
227
+ taxonomy: z.string().nullable(),
232
228
  hasVariations: z.boolean(),
233
229
  terms: z.array(ProductAttributeTermRef)
234
230
  });
@@ -753,10 +749,7 @@ const CART_ROUTER = {
753
749
  errors: GET_CART_ERROR_MAP,
754
750
  middlewares: [sessionMiddleware()]
755
751
  }, async ({ context, errors }) => {
756
- const response = await context.wordpress.get("/cart", {
757
- base: WC_STORE_BASE,
758
- headers: context.sessionHeaders
759
- });
752
+ const response = await context.wordpress.woocommerce.store.cart.get({}, { headers: context.sessionHeaders });
760
753
  if (response.error) switch (response.error.code) {
761
754
  default:
762
755
  context.logger.error("Get cart unhandled error", response.error, { code: response.error.code });
@@ -774,45 +767,41 @@ const CART_ROUTER = {
774
767
  middlewares: [sessionMiddleware()]
775
768
  }, async ({ context, input: { body: input }, errors }) => {
776
769
  const connInfo = await context.getConnInfo();
777
- const defaultBilling = input.billing ?? {
770
+ const billing = input.billing ?? {
778
771
  ...input.shipping,
779
772
  email: void 0
780
773
  };
781
- const updateData = {
782
- billing_address: defaultBilling !== void 0 ? {
783
- first_name: defaultBilling?.firstName ?? "",
784
- last_name: defaultBilling?.lastName ?? "",
785
- address_1: defaultBilling?.address1 ?? "",
786
- address_2: defaultBilling?.address2 ?? "",
787
- company: defaultBilling?.company ?? "",
788
- email: defaultBilling?.email ?? "",
789
- phone: defaultBilling?.phone ?? "",
790
- city: defaultBilling?.city ?? "",
791
- state: defaultBilling?.state ?? connInfo?.state ?? void 0,
792
- country: defaultBilling?.country ?? connInfo?.country ?? void 0,
793
- postcode: defaultBilling?.postcode ?? connInfo?.postcode ?? ""
794
- } : {},
795
- shipping_address: input.shipping !== void 0 ? {
796
- first_name: input.shipping?.firstName ?? "",
797
- last_name: input.shipping?.lastName ?? "",
798
- address_1: input.shipping?.address1 ?? "",
799
- address_2: input.shipping?.address2 ?? "",
800
- company: input.shipping?.company ?? "",
801
- phone: input.shipping?.phone ?? "",
802
- city: input.shipping?.city ?? "",
803
- state: input.shipping?.state ?? connInfo?.state ?? void 0,
804
- country: input.shipping?.country ?? connInfo?.country ?? void 0,
805
- postcode: input.shipping?.postcode ?? connInfo?.postcode ?? ""
806
- } : {}
774
+ const address = {
775
+ first_name: billing.firstName ?? "",
776
+ last_name: billing.lastName ?? "",
777
+ address_1: billing.address1 ?? "",
778
+ address_2: billing.address2 ?? "",
779
+ company: billing.company ?? "",
780
+ phone: billing.phone ?? "",
781
+ city: billing.city ?? "",
782
+ state: billing.state ?? connInfo?.state ?? "",
783
+ country: billing.country ?? connInfo?.country ?? "",
784
+ postcode: billing.postcode ?? connInfo?.postcode ?? ""
807
785
  };
808
- const response = await context.wordpress.post("/cart/update-customer", {
809
- base: WC_STORE_BASE,
810
- body: {
811
- billing_address: updateData.billing_address,
812
- shipping_address: updateData.shipping_address
786
+ const response = await context.wordpress.woocommerce.store.cart.updateCustomer({
787
+ billing_address: {
788
+ ...address,
789
+ email: billing.email ?? ""
813
790
  },
814
- headers: context.sessionHeaders
815
- });
791
+ ...input.shipping && { shipping_address: {
792
+ ...address,
793
+ first_name: input.shipping.firstName ?? "",
794
+ last_name: input.shipping.lastName ?? "",
795
+ address_1: input.shipping.address1 ?? "",
796
+ address_2: input.shipping.address2 ?? "",
797
+ company: input.shipping.company ?? "",
798
+ phone: input.shipping.phone ?? "",
799
+ city: input.shipping.city ?? "",
800
+ state: input.shipping.state ?? connInfo?.state ?? "",
801
+ country: input.shipping.country ?? connInfo?.country ?? "",
802
+ postcode: input.shipping.postcode ?? connInfo?.postcode ?? ""
803
+ } }
804
+ }, { headers: context.sessionHeaders });
816
805
  if (response.error) switch (response.error.code) {
817
806
  default:
818
807
  context.logger.error("Update cart customer unhandled error", response.error, { code: response.error.code });
@@ -829,14 +818,10 @@ const CART_ROUTER = {
829
818
  errors: SELECT_SHIPPING_RATE_ERROR_MAP,
830
819
  middlewares: [sessionMiddleware()]
831
820
  }, async ({ context, input: { body }, errors }) => {
832
- const response = await context.wordpress.post("/cart/select-shipping-rate", {
833
- base: WC_STORE_BASE,
834
- headers: context.sessionHeaders,
835
- body: {
836
- rate_id: body.rateId,
837
- package_id: body.packageId
838
- }
839
- });
821
+ const response = await context.wordpress.woocommerce.store.cart.selectShippingRate({
822
+ rate_id: body.rateId,
823
+ package_id: body.packageId
824
+ }, { headers: context.sessionHeaders });
840
825
  if (response.error) switch (response.error.code) {
841
826
  case "woocommerce_rest_cart_shipping_rate_not_found": throw errors.CART_SHIPPING_RATE_NOT_FOUND({ message: response.error.message });
842
827
  case "woocommerce_rest_shipping_disabled": throw errors.CART_SHIPPING_DISABLED({ message: response.error.message });
@@ -856,15 +841,11 @@ const CART_ROUTER = {
856
841
  errors: ADD_CART_ITEM_ERROR_MAP,
857
842
  middlewares: [sessionMiddleware()]
858
843
  }, async ({ context, input: { body: input }, errors }) => {
859
- const response = await context.wordpress.post("/cart/add-item", {
860
- body: {
861
- id: input.productId,
862
- quantity: input.quantity,
863
- variation: input.variations ?? []
864
- },
865
- base: WC_STORE_BASE,
866
- headers: context.sessionHeaders
867
- });
844
+ const response = await context.wordpress.woocommerce.store.cart.addItem({
845
+ id: input.productId,
846
+ quantity: input.quantity,
847
+ variation: input.variations ?? []
848
+ }, { headers: context.sessionHeaders });
868
849
  if (response.error) switch (response.error.code) {
869
850
  case "woocommerce_rest_product_out_of_stock": throw errors.CART_ITEM_OUT_OF_STOCK({ message: response.error.message });
870
851
  case "woocommerce_rest_product_partially_out_of_stock": throw errors.CART_ITEM_INSUFFICIENT_STOCK({ message: response.error.message });
@@ -893,14 +874,10 @@ const CART_ROUTER = {
893
874
  errors: UPDATE_CART_ITEM_ERROR_MAP,
894
875
  middlewares: [sessionMiddleware()]
895
876
  }, async ({ context, input: { params, body }, errors }) => {
896
- const response = await context.wordpress.post("/cart/update-item", {
897
- body: {
898
- key: params.key,
899
- quantity: body.quantity
900
- },
901
- base: WC_STORE_BASE,
902
- headers: context.sessionHeaders
903
- });
877
+ const response = await context.wordpress.woocommerce.store.cart.updateItem({
878
+ key: params.key,
879
+ quantity: body.quantity
880
+ }, { headers: context.sessionHeaders });
904
881
  if (response.error) switch (response.error.code) {
905
882
  case "woocommerce_rest_cart_invalid_key": throw errors.CART_ITEM_NOT_FOUND({ message: response.error.message });
906
883
  case "woocommerce_rest_cart_invalid_product": throw errors.CART_PRODUCT_INVALID({ message: response.error.message });
@@ -922,11 +899,7 @@ const CART_ROUTER = {
922
899
  errors: REMOVE_CART_ITEM_ERROR_MAP,
923
900
  middlewares: [sessionMiddleware()]
924
901
  }, async ({ context, input: { params }, errors }) => {
925
- const response = await context.wordpress.post("/cart/remove-item", {
926
- body: { key: params.key },
927
- base: WC_STORE_BASE,
928
- headers: context.sessionHeaders
929
- });
902
+ const response = await context.wordpress.woocommerce.store.cart.removeItem({ key: params.key }, { headers: context.sessionHeaders });
930
903
  if (response.error) switch (response.error.code) {
931
904
  case "woocommerce_rest_cart_invalid_key": throw errors.CART_ITEM_NOT_FOUND({ message: response.error.message });
932
905
  default:
@@ -946,11 +919,7 @@ const CART_ROUTER = {
946
919
  errors: APPLY_COUPON_ERROR_MAP,
947
920
  middlewares: [sessionMiddleware()]
948
921
  }, async ({ context, input: { body }, errors }) => {
949
- const response = await context.wordpress.post("/cart/apply-coupon", {
950
- body: { code: body.code },
951
- base: WC_STORE_BASE,
952
- headers: context.sessionHeaders
953
- });
922
+ const response = await context.wordpress.woocommerce.store.cart.applyCoupon({ code: body.code }, { headers: context.sessionHeaders });
954
923
  if (response.error) switch (response.error.code) {
955
924
  case "woocommerce_rest_cart_coupon_error": throw errors.CART_COUPON_INVALID({ message: response.error.message });
956
925
  case "woocommerce_rest_cart_coupon_disabled": throw errors.CART_COUPON_DISABLED({ message: response.error.message });
@@ -969,11 +938,7 @@ const CART_ROUTER = {
969
938
  errors: REMOVE_COUPON_ERROR_MAP,
970
939
  middlewares: [sessionMiddleware()]
971
940
  }, async ({ context, input, errors }) => {
972
- const response = await context.wordpress.post("/cart/remove-coupon", {
973
- body: { code: input.params.code },
974
- base: WC_STORE_BASE,
975
- headers: context.sessionHeaders
976
- });
941
+ const response = await context.wordpress.woocommerce.store.cart.removeCoupon({ code: input.params.code }, { headers: context.sessionHeaders });
977
942
  if (response.error) switch (response.error.code) {
978
943
  case "woocommerce_rest_cart_coupon_error": throw errors.CART_COUPON_INVALID({ message: response.error.message });
979
944
  case "woocommerce_rest_cart_coupon_disabled": throw errors.CART_COUPON_DISABLED({ message: response.error.message });
@@ -1175,13 +1140,61 @@ const RetryCheckoutInput = z$2.object({
1175
1140
  orderId: NumberLike,
1176
1141
  paymentMethod: z$2.string(),
1177
1142
  billingEmail: z$2.email().optional(),
1178
- billingAddress: BillingAddress.optional(),
1143
+ billingAddress: BillingAddress,
1179
1144
  paymentData: CheckoutPaymentData.optional(),
1180
1145
  shippingAddress: ShippingAddress.optional()
1181
1146
  });
1182
1147
 
1183
1148
  //#endregion
1184
1149
  //#region src/checkout/utils.ts
1150
+ /**
1151
+ * A payment method the caller named, passed to WooCommerce as one.
1152
+ *
1153
+ * WooCommerce builds this argument's enum from `get_payment_gateway_ids()`, so the contract
1154
+ * enumerates the gateways enabled on the WordPress the client was generated against. That is true of
1155
+ * that install and says nothing about the one a build actually talks to, and WooCommerce agrees: its
1156
+ * own comment on the enum is that further validation happens during the request.
1157
+ *
1158
+ * So the enum is documentation here rather than a constraint, and the gateway a shopper chose is
1159
+ * sent whatever it is. WooCommerce rejects an unknown one with
1160
+ * `woocommerce_rest_checkout_payment_method_disabled`, which both callers already map.
1161
+ */
1162
+ function gateway(method) {
1163
+ return method;
1164
+ }
1165
+ /**
1166
+ * A Kizlo address in the shape WooCommerce registered its argument in.
1167
+ *
1168
+ * Kizlo's addresses are camelCase and WooCommerce's are snake_case, and until the contract described
1169
+ * the route nothing said so: the retry call passed a Kizlo address straight through as
1170
+ * `billing_address`, where every key missed and WooCommerce kept the address already on the order.
1171
+ *
1172
+ * Every field is required there, so an address given in part is sent filled out. An address that was
1173
+ * not given at all is a different thing and never reaches here: WooCommerce reads an absent
1174
+ * `shipping_address` as "use the billing address", and a blank one as an address, so a caller who
1175
+ * omitted it must leave the argument off the call rather than send this filled with empty strings.
1176
+ */
1177
+ function serializeAddress(address) {
1178
+ return {
1179
+ first_name: address.firstName,
1180
+ last_name: address.lastName,
1181
+ address_1: address.address1,
1182
+ address_2: address.address2 ?? "",
1183
+ company: address.company ?? "",
1184
+ city: address.city,
1185
+ state: address.state,
1186
+ country: address.country,
1187
+ postcode: address.postcode,
1188
+ phone: address.phone
1189
+ };
1190
+ }
1191
+ /** The same, plus the email WooCommerce carries on the billing address alone. */
1192
+ function serializeBillingAddress(address) {
1193
+ return {
1194
+ ...serializeAddress(address),
1195
+ email: address.email
1196
+ };
1197
+ }
1185
1198
  function deserializeCheckout(data) {
1186
1199
  return {
1187
1200
  cart: data.__experimentalCart ? deserializeCart(data.__experimentalCart) : null,
@@ -1210,16 +1223,40 @@ function deserializeCheckout(data) {
1210
1223
  postcode: data.billing_address.postcode,
1211
1224
  state: data.billing_address.state
1212
1225
  },
1213
- additionalFields: data.additional_fields,
1226
+ additionalFields: additionalFields(data.additional_fields),
1214
1227
  customerNote: data.customer_note,
1215
1228
  paymentMethod: data.payment_method,
1216
1229
  paymentResult: data.payment_result?.payment_status ? {
1217
- status: data.payment_result.payment_status,
1230
+ status: paymentStatus(data.payment_result.payment_status),
1218
1231
  redirectUrl: data.payment_result.redirect_url,
1219
1232
  data: data.payment_result.payment_details
1220
1233
  } : null
1221
1234
  };
1222
1235
  }
1236
+ /**
1237
+ * WooCommerce registers `additional_fields` as a bare object, so the contract describes it as one
1238
+ * with undescribed contents: what a site's checkout fields are is a per-site question that no schema
1239
+ * can answer ahead of time. The values are scalars in practice, and anything else is dropped rather
1240
+ * than passed on to fail the procedure's own output validation with a less useful message.
1241
+ */
1242
+ function additionalFields(fields) {
1243
+ const kept = {};
1244
+ for (const [key, value] of Object.entries(fields ?? {})) if (typeof value === "string" || typeof value === "boolean") kept[key] = value;
1245
+ return kept;
1246
+ }
1247
+ /**
1248
+ * WooCommerce names the four statuses in the field's description and declares the field a plain
1249
+ * string, so the contract cannot narrow it. `error` is the safe reading of anything unrecognized: a
1250
+ * gateway that answered with something new has not taken payment as far as this checkout knows.
1251
+ */
1252
+ function paymentStatus(status) {
1253
+ switch (status) {
1254
+ case "success":
1255
+ case "pending":
1256
+ case "failure": return status;
1257
+ default: return "error";
1258
+ }
1259
+ }
1223
1260
 
1224
1261
  //#endregion
1225
1262
  //#region src/checkout/index.ts
@@ -1232,10 +1269,7 @@ const CHECKOUT_ROUTER = {
1232
1269
  errors: GET_CHECKOUT_ERROR_MAP,
1233
1270
  middlewares: [sessionMiddleware()]
1234
1271
  }, async ({ context, errors }) => {
1235
- const response = await context.wordpress.get("/checkout", {
1236
- base: WC_STORE_BASE,
1237
- headers: context.sessionHeaders
1238
- });
1272
+ const response = await context.wordpress.woocommerce.store.checkout.get({}, { headers: context.sessionHeaders });
1239
1273
  if (response.error) switch (response.error.code) {
1240
1274
  case "woocommerce_rest_checkout_missing_order": throw errors.CHECKOUT_ORDER_NOT_FOUND({ message: response.error.message });
1241
1275
  default:
@@ -1253,16 +1287,12 @@ const CHECKOUT_ROUTER = {
1253
1287
  errors: UPDATE_CHECKOUT_ERROR_MAP,
1254
1288
  middlewares: [sessionMiddleware()]
1255
1289
  }, async ({ context, input, errors }) => {
1256
- const response = await context.wordpress.put("/checkout", {
1257
- base: WC_STORE_BASE,
1258
- body: {
1259
- order_notes: input.body.customerNote,
1260
- payment_method: input.body.paymentMethod,
1261
- additional_fields: input.body.additionalFields,
1262
- __experimental_calc_totals: input.body.recalculateTotals
1263
- },
1264
- headers: context.sessionHeaders
1265
- });
1290
+ const response = await context.wordpress.woocommerce.store.checkout.update({
1291
+ order_notes: input.body.customerNote,
1292
+ payment_method: gateway(input.body.paymentMethod),
1293
+ additional_fields: input.body.additionalFields,
1294
+ __experimental_calc_totals: input.body.recalculateTotals
1295
+ }, { headers: context.sessionHeaders });
1266
1296
  if (response.error) switch (response.error.code) {
1267
1297
  case "woocommerce_rest_cart_coupon_error": throw errors.CHECKOUT_COUPON_INVALID({ message: response.error.message });
1268
1298
  case "woocommerce_rest_checkout_payment_method_disabled": throw errors.CHECKOUT_PAYMENT_METHOD_DISABLED({ message: response.error.message });
@@ -1287,30 +1317,23 @@ const CHECKOUT_ROUTER = {
1287
1317
  errors: CONFIRM_CHECKOUT_ERROR_MAP,
1288
1318
  middlewares: [sessionMiddleware()]
1289
1319
  }, async ({ context, input, errors }) => {
1290
- const checkoutResponse = await context.wordpress.get("/checkout", {
1291
- base: WC_STORE_BASE,
1292
- headers: context.sessionHeaders
1293
- });
1320
+ const checkoutResponse = await context.wordpress.woocommerce.store.checkout.get({}, { headers: context.sessionHeaders });
1294
1321
  if (checkoutResponse.error) switch (checkoutResponse.error.code) {
1295
1322
  case "woocommerce_rest_checkout_missing_order": throw errors.CHECKOUT_ORDER_NOT_FOUND({ message: checkoutResponse.error.message });
1296
1323
  default:
1297
1324
  context.logger.error("Get checkout for confirm unhandled error", checkoutResponse.error, { code: checkoutResponse.error.code });
1298
1325
  throw errors.INTERNAL_SERVER_ERROR();
1299
1326
  }
1300
- const confirmResponse = await context.wordpress.post("/checkout", {
1301
- base: WC_STORE_BASE,
1302
- body: {
1303
- payment_data: input.body.paymentData,
1304
- customer_password: input.body.customerPassword,
1305
- customer_note: checkoutResponse.data.customer_note,
1306
- payment_method: checkoutResponse.data.payment_method,
1307
- create_account: !!input.body.customerPassword?.length,
1308
- billing_address: checkoutResponse.data.billing_address,
1309
- shipping_address: checkoutResponse.data.shipping_address,
1310
- additional_fields: checkoutResponse.data.additional_fields
1311
- },
1312
- headers: context.sessionHeaders
1313
- });
1327
+ const confirmResponse = await context.wordpress.woocommerce.store.checkout.process({
1328
+ payment_data: input.body.paymentData,
1329
+ customer_password: input.body.customerPassword,
1330
+ customer_note: checkoutResponse.data.customer_note,
1331
+ payment_method: checkoutResponse.data.payment_method,
1332
+ create_account: !!input.body.customerPassword?.length,
1333
+ billing_address: checkoutResponse.data.billing_address,
1334
+ shipping_address: checkoutResponse.data.shipping_address,
1335
+ additional_fields: checkoutResponse.data.additional_fields
1336
+ }, { headers: context.sessionHeaders });
1314
1337
  if (confirmResponse.error) switch (confirmResponse.error.code) {
1315
1338
  case "woocommerce_rest_invalid_address": throw errors.CHECKOUT_ADDRESS_INVALID({ message: confirmResponse.error.message });
1316
1339
  case "woocommerce_rest_invalid_address_country": throw errors.CHECKOUT_ADDRESS_COUNTRY_INVALID({ message: confirmResponse.error.message });
@@ -1346,19 +1369,15 @@ const CHECKOUT_ROUTER = {
1346
1369
  errors: RETRY_CHECKOUT_ERROR_MAP,
1347
1370
  middlewares: [sessionMiddleware()]
1348
1371
  }, async ({ context, input, errors }) => {
1349
- const response = await context.wordpress.post(`/checkout/${input.params.orderId}`, {
1350
- base: WC_STORE_BASE,
1351
- body: {
1352
- key: input.body.key,
1353
- id: input.params.orderId,
1354
- payment_data: input.body.paymentData,
1355
- billing_email: input.body.billingEmail,
1356
- payment_method: input.body.paymentMethod,
1357
- billing_address: input.body.billingAddress ?? {},
1358
- shipping_address: input.body.shippingAddress ?? {}
1359
- },
1360
- headers: context.sessionHeaders
1361
- });
1372
+ const response = await context.wordpress.woocommerce.store.checkout.processOrder({
1373
+ key: input.body.key,
1374
+ id: input.params.orderId,
1375
+ payment_data: input.body.paymentData,
1376
+ billing_email: input.body.billingEmail,
1377
+ payment_method: gateway(input.body.paymentMethod),
1378
+ billing_address: serializeBillingAddress(input.body.billingAddress),
1379
+ shipping_address: input.body.shippingAddress ? serializeAddress(input.body.shippingAddress) : void 0
1380
+ }, { headers: context.sessionHeaders });
1362
1381
  if (response.error) switch (response.error.code) {
1363
1382
  case "woocommerce_rest_invalid_billing_email": throw errors.CHECKOUT_EMAIL_INVALID({ message: response.error.message });
1364
1383
  case "woocommerce_rest_checkout_process_payment_error": throw errors.CHECKOUT_PAYMENT_FAILED({ message: response.error.message });
@@ -1447,8 +1466,17 @@ const CUSTOMER_ROUTER = { get: createProcedure({
1447
1466
  }, async ({ context, errors }) => {
1448
1467
  const auth = await context.getAuthUser();
1449
1468
  if (!auth) throw errors.FORBIDDEN();
1450
- const response = await context.wordpress.get(`/customers/${auth.id}`, { base: WC_CORE_BASE });
1451
- if (response.error) throw response.error;
1469
+ const response = await context.wordpress.woocommerce.customers.retrieve({ id: auth.id });
1470
+ if (response.error) switch (response.error.code) {
1471
+ case "wc_user_invalid_id": throw errors.NOT_FOUND();
1472
+ case "woocommerce_rest_cannot_view": throw errors.FORBIDDEN();
1473
+ default:
1474
+ context.logger.error("Get customer unhandled error", response.error, {
1475
+ userId: auth.id,
1476
+ code: response.error.code
1477
+ });
1478
+ throw errors.INTERNAL_SERVER_ERROR();
1479
+ }
1452
1480
  return deserializeCustomer(response.data);
1453
1481
  }) };
1454
1482
 
@@ -1472,13 +1500,13 @@ function deserializeProduct(data) {
1472
1500
  description: data.description,
1473
1501
  shortDescription: data.short_description,
1474
1502
  prices: {
1475
- price: +data.kizlo.prices.price,
1476
- salePrice: +data.kizlo.prices.sale_price,
1477
- regularPrice: +data.kizlo.prices.regular_price
1503
+ price: data.kizlo.prices.price,
1504
+ salePrice: data.kizlo.prices.sale_price,
1505
+ regularPrice: data.kizlo.prices.regular_price
1478
1506
  },
1479
1507
  isSoldIndividually: data.sold_individually,
1480
1508
  onSaleFrom: data.date_on_sale_from ? toTimestamp(data.date_on_sale_from) : null,
1481
- lowStockRemaining: "data.low_stock_amount",
1509
+ lowStockRemaining: data.low_stock_amount,
1482
1510
  onSaleTo: data.date_on_sale_to ? toTimestamp(data.date_on_sale_to) : null,
1483
1511
  isInStock: data.stock_status === "instock",
1484
1512
  stock: data.stock_quantity,
@@ -1508,9 +1536,15 @@ function deserializeProduct(data) {
1508
1536
  };
1509
1537
  }
1510
1538
  function deserializeStoreProduct(data) {
1539
+ const kizlo = data.extensions.kizlo ?? {
1540
+ stock: null,
1541
+ on_sale_from: null,
1542
+ on_sale_to: null,
1543
+ hs_code: null
1544
+ };
1511
1545
  return {
1512
1546
  id: data.id,
1513
- type: data.type,
1547
+ type: productType(data.type),
1514
1548
  name: data.name,
1515
1549
  slug: data.slug,
1516
1550
  sku: data.sku,
@@ -1545,9 +1579,9 @@ function deserializeStoreProduct(data) {
1545
1579
  isOnSale: data.on_sale,
1546
1580
  parentId: data.parent,
1547
1581
  variations: data.variations,
1548
- stock: data.extensions.kizlo.stock,
1549
- onSaleFrom: data.extensions.kizlo.on_sale_from ? toTimestamp(data.extensions.kizlo.on_sale_from) : null,
1550
- onSaleTo: data.extensions.kizlo.on_sale_to ? toTimestamp(data.extensions.kizlo.on_sale_to) : null,
1582
+ stock: kizlo.stock,
1583
+ onSaleFrom: kizlo.on_sale_from ? toTimestamp(kizlo.on_sale_from) : null,
1584
+ onSaleTo: kizlo.on_sale_to ? toTimestamp(kizlo.on_sale_to) : null,
1551
1585
  seo: null,
1552
1586
  meta: {}
1553
1587
  };
@@ -1557,7 +1591,13 @@ function deserializeProductFilters(data) {
1557
1591
  const maxPrice = +data.price_range.max_price;
1558
1592
  const minPrice = +data.price_range.min_price;
1559
1593
  return {
1560
- stockStatuses: data.stock_status_counts ?? [],
1594
+ stockStatuses: (data.stock_status_counts ?? []).flatMap((entry) => {
1595
+ const status = stockStatus(entry.status);
1596
+ return status ? [{
1597
+ count: entry.count,
1598
+ status
1599
+ }] : [];
1600
+ }),
1561
1601
  taxonomyTerms: data.kizlo.taxonomy_counts.map((item) => ({
1562
1602
  id: item.id,
1563
1603
  name: item.name,
@@ -1608,7 +1648,7 @@ function serializeProductListInput(data) {
1608
1648
  orderby: data?.orderby,
1609
1649
  parent: normalizeArrayableValue(data?.parent),
1610
1650
  parent_exclude: normalizeArrayableValue(data?.parentExclude),
1611
- rating: normalizeArrayableValue(data?.rating)?.map(Number),
1651
+ rating: normalizeArrayableValue(data?.rating)?.map((value) => Number(value)),
1612
1652
  sku: data?.sku,
1613
1653
  slug: data?.slug,
1614
1654
  stock_status: normalizeArrayableValue(data?.stockStatus),
@@ -1631,6 +1671,20 @@ function serializeProductListInput(data) {
1631
1671
  search: data?.search
1632
1672
  };
1633
1673
  }
1674
+ /**
1675
+ * WooCommerce declares both of these fields as plain strings, so the contract cannot narrow them and
1676
+ * neither can this without saying what it does with a value it does not recognize.
1677
+ *
1678
+ * A product type Kizlo has no name for reads as `simple`, which is what every gateway to a product
1679
+ * page needs it to behave like. An unrecognized stock status is dropped from the filter list
1680
+ * instead: a count nobody can label is not a filter anyone can offer.
1681
+ */
1682
+ function productType(type) {
1683
+ return PRODUCT_TYPES.includes(type) ? type : "simple";
1684
+ }
1685
+ function stockStatus(status) {
1686
+ return PRODUCT_STOCK_STATUSES.includes(status) ? status : null;
1687
+ }
1634
1688
  function deserializeImages(images) {
1635
1689
  return images.map((item) => ({
1636
1690
  id: item.id,
@@ -1665,7 +1719,7 @@ const PRODUCT_ROUTER = {
1665
1719
  if (input.query?.previewToken) {
1666
1720
  const result = await context.verifyPreviewToken(input.query.previewToken);
1667
1721
  if (!result) throw errors.PRODUCT_NOT_FOUND();
1668
- const response$1 = await context.wordpress.get(`/products/${result.id}`, { base: WC_CORE_BASE });
1722
+ const response$1 = await context.wordpress.woocommerce.products.retrieve({ id: Number(result.id) });
1669
1723
  if (response$1.error) switch (response$1.error.code) {
1670
1724
  case "woocommerce_rest_product_invalid_id": throw errors.PRODUCT_NOT_FOUND({ message: response$1.error.message });
1671
1725
  default:
@@ -1677,10 +1731,7 @@ const PRODUCT_ROUTER = {
1677
1731
  }
1678
1732
  return deserializeProduct(response$1.data);
1679
1733
  }
1680
- const response = await context.wordpress.get("/products", {
1681
- searchParams: { slug: input.params.identifier },
1682
- base: WC_CORE_BASE
1683
- });
1734
+ const response = await context.wordpress.woocommerce.products.list({ slug: String(input.params.identifier) });
1684
1735
  if (response.error) switch (response.error.code) {
1685
1736
  default:
1686
1737
  context.logger.error("Get product unhandled error", response.error, {
@@ -1702,10 +1753,7 @@ const PRODUCT_ROUTER = {
1702
1753
  errors: LIST_PRODUCT_ERROR_MAP
1703
1754
  }, async ({ context, input, errors }) => {
1704
1755
  const searchParams = serializeProductListInput(input.query);
1705
- const response = await context.wordpress.get("/products", {
1706
- base: WC_STORE_BASE,
1707
- searchParams: { ...searchParams }
1708
- });
1756
+ const response = await context.wordpress.woocommerce.store.products.list(searchParams);
1709
1757
  if (response.error) switch (response.error.code) {
1710
1758
  default:
1711
1759
  context.logger.error("List products unhandled error", response.error, { code: response.error.code });
@@ -1729,19 +1777,16 @@ const PRODUCT_ROUTER = {
1729
1777
  output: ProductFilters.nullable()
1730
1778
  }, async ({ context, errors, input }) => {
1731
1779
  const searchParams = serializeProductListInput(input.query);
1732
- const response = await context.wordpress.get("/products/collection-data", {
1733
- base: WC_STORE_BASE,
1734
- searchParams: {
1735
- ...searchParams,
1736
- calculate_price_range: true,
1737
- calculate_rating_counts: input.query?.ratingFilters,
1738
- calculate_taxonomy_counts: input.query?.taxonomyFilters,
1739
- calculate_stock_status_counts: input.query?.stockStatusFilters,
1740
- calculate_attribute_counts: input.query?.attributeFilters?.map((item) => ({
1741
- taxonomy: item.taxonomy,
1742
- query_type: item.queryType
1743
- }))
1744
- }
1780
+ const response = await context.wordpress.woocommerce.store.products.collectionData({
1781
+ ...searchParams,
1782
+ calculate_price_range: true,
1783
+ calculate_rating_counts: input.query?.ratingFilters,
1784
+ calculate_taxonomy_counts: input.query?.taxonomyFilters,
1785
+ calculate_stock_status_counts: input.query?.stockStatusFilters,
1786
+ calculate_attribute_counts: input.query?.attributeFilters?.map((item) => ({
1787
+ taxonomy: item.taxonomy,
1788
+ query_type: item.queryType
1789
+ }))
1745
1790
  });
1746
1791
  if (response.error) switch (response.error.code) {
1747
1792
  default:
@@ -1757,6 +1802,21 @@ const PRODUCT_ROUTER = {
1757
1802
  function woocommerce() {
1758
1803
  return createExtension({
1759
1804
  id: "woocommerce",
1805
+ requires: {
1806
+ plugin: {
1807
+ slug: "kizlo-woocommerce",
1808
+ name: "Kizlo WooCommerce",
1809
+ version: "0.2.0"
1810
+ },
1811
+ endpoints: [
1812
+ "woocommerce.customers",
1813
+ "woocommerce.products",
1814
+ "woocommerce.kizlo.cart",
1815
+ "woocommerce.store.cart",
1816
+ "woocommerce.store.checkout",
1817
+ "woocommerce.store.products"
1818
+ ]
1819
+ },
1760
1820
  init: () => {
1761
1821
  return { router: {
1762
1822
  cart: CART_ROUTER,
package/dist/test.js CHANGED
@@ -1,6 +1,15 @@
1
- import { WC_CORE_BASE, WC_STORE_BASE } from "kizlo";
2
1
  import { defineFixture, kizloRelease } from "kizlo/test";
3
2
 
3
+ //#region src/constants.ts
4
+ /**
5
+ * Base paths for the two WooCommerce APIs, used only where this package seeds or inspects a store
6
+ * directly. Everything a router or service reaches goes through a generated endpoint, which derives
7
+ * its own prefix from the namespace the plugin declares.
8
+ */
9
+ const WC_CORE_BASE = "/wp-json/wc/v3";
10
+ const WC_STORE_BASE = "/wp-json/wc/store/v1";
11
+
12
+ //#endregion
4
13
  //#region src/test/index.ts
5
14
  const PRODUCTS = [
6
15
  {
@@ -51,6 +60,30 @@ async function upsertCoupon(service, coupon) {
51
60
  if (created.error) throw created.error;
52
61
  }
53
62
  /**
63
+ * WooCommerce ships every gateway disabled, and paying an order needs an available one, so the
64
+ * checkout tests have nothing to reach without this. Bank transfer is the gateway with no shipping
65
+ * method or country conditions attached to it, so enabling it says the least about the rest.
66
+ */
67
+ async function enableBankTransfer(service) {
68
+ const updated = await service.put(`${WC_CORE_BASE}/payment_gateways/bacs`, { body: { enabled: true } });
69
+ if (updated.error) throw updated.error;
70
+ }
71
+ /**
72
+ * WooCommerce bundles Action Scheduler, which keeps its actions in the posts table until it
73
+ * migrates to its own tables, and registers two global post statuses (`in-progress`, `failed`)
74
+ * for as long as that legacy store is live. The migration is scheduled a minute after activation
75
+ * and left to cron, so a WordPress reports those two statuses for roughly its first minute and
76
+ * never again. Every post type's status enum is built from the global list, so a client generated
77
+ * inside that window carries two statuses that one generated outside it does not.
78
+ *
79
+ * Firing Action Scheduler's own migration callback settles it: the runner finds nothing to move
80
+ * on a fresh install and marks the migration complete itself, so this is its cron path run now
81
+ * rather than its bookkeeping written by hand.
82
+ */
83
+ async function settleActionScheduler(wpEval) {
84
+ await wpEval("if (class_exists(\"ActionScheduler_DataController\") && !ActionScheduler_DataController::is_migration_complete()) { do_action(\"action_scheduler/migration_hook\"); }");
85
+ }
86
+ /**
54
87
  * WooCommerce test fixture: installs WooCommerce + the kizlo-woocommerce plugin, seeds
55
88
  * products/coupons. Pass `plugins` to override the defaults — e.g. bind-mount your local
56
89
  * source with `{ path: "plugins/kizlo-woocommerce" }` to develop/test against live files.
@@ -62,6 +95,9 @@ function woocommerce(opts = {}) {
62
95
  name: "kizlo-woocommerce",
63
96
  source: kizloRelease("kizlo-woocommerce")
64
97
  }],
98
+ async settle({ wpEval }) {
99
+ await settleActionScheduler(wpEval);
100
+ },
65
101
  async seed({ service }) {
66
102
  let productId = 0;
67
103
  for (const product of PRODUCTS) {
@@ -69,6 +105,7 @@ function woocommerce(opts = {}) {
69
105
  if (!productId) productId = id;
70
106
  }
71
107
  for (const coupon of COUPONS) await upsertCoupon(service, coupon);
108
+ await enableBankTransfer(service);
72
109
  return { productId };
73
110
  },
74
111
  async cleanup({ service, userId }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kizlo/woocommerce",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "WooCommerce extension for Kizlo.",
@@ -49,11 +49,11 @@
49
49
  "access": "public"
50
50
  },
51
51
  "dependencies": {
52
- "@kizlo/shared": "0.6.0",
52
+ "@kizlo/shared": "0.7.0",
53
53
  "jose": "6.1.0",
54
54
  "zod": "^4.3.6"
55
55
  },
56
56
  "peerDependencies": {
57
- "kizlo": ">=0.1.0"
57
+ "kizlo": ">=0.15.0"
58
58
  }
59
59
  }