@kizlo/woocommerce 0.1.9 → 0.2.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/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,4 +1,4 @@
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";
@@ -80,14 +80,11 @@ function sessionMiddleware(options) {
80
80
  if (foundToken) {
81
81
  const [err, data] = await tryCatch(verifyToken(foundToken, context.config.siteSecret));
82
82
  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
- });
83
+ const response = await context.wordpress.woocommerce.kizlo.cart.merge({}, { headers: getCartHeaders({
84
+ userId: auth.id,
85
+ token: data.sub,
86
+ connInfo
87
+ }) });
91
88
  if (response.error) context.logger.error("CART_MERGE_FAILED", response.error);
92
89
  }
93
90
  await context.cookies.delete(cookieName);
@@ -228,7 +225,7 @@ const ProductAttributeTermRef = z.object({
228
225
  const ProductAttributeRef = z.object({
229
226
  id: z.number(),
230
227
  name: z.string(),
231
- taxonomy: z.string(),
228
+ taxonomy: z.string().nullable(),
232
229
  hasVariations: z.boolean(),
233
230
  terms: z.array(ProductAttributeTermRef)
234
231
  });
@@ -753,10 +750,7 @@ const CART_ROUTER = {
753
750
  errors: GET_CART_ERROR_MAP,
754
751
  middlewares: [sessionMiddleware()]
755
752
  }, async ({ context, errors }) => {
756
- const response = await context.wordpress.get("/cart", {
757
- base: WC_STORE_BASE,
758
- headers: context.sessionHeaders
759
- });
753
+ const response = await context.wordpress.woocommerce.store.cart.get({}, { headers: context.sessionHeaders });
760
754
  if (response.error) switch (response.error.code) {
761
755
  default:
762
756
  context.logger.error("Get cart unhandled error", response.error, { code: response.error.code });
@@ -774,45 +768,41 @@ const CART_ROUTER = {
774
768
  middlewares: [sessionMiddleware()]
775
769
  }, async ({ context, input: { body: input }, errors }) => {
776
770
  const connInfo = await context.getConnInfo();
777
- const defaultBilling = input.billing ?? {
771
+ const billing = input.billing ?? {
778
772
  ...input.shipping,
779
773
  email: void 0
780
774
  };
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
- } : {}
775
+ const address = {
776
+ first_name: billing.firstName ?? "",
777
+ last_name: billing.lastName ?? "",
778
+ address_1: billing.address1 ?? "",
779
+ address_2: billing.address2 ?? "",
780
+ company: billing.company ?? "",
781
+ phone: billing.phone ?? "",
782
+ city: billing.city ?? "",
783
+ state: billing.state ?? connInfo?.state ?? "",
784
+ country: billing.country ?? connInfo?.country ?? "",
785
+ postcode: billing.postcode ?? connInfo?.postcode ?? ""
807
786
  };
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
787
+ const response = await context.wordpress.woocommerce.store.cart.updateCustomer({
788
+ billing_address: {
789
+ ...address,
790
+ email: billing.email ?? ""
813
791
  },
814
- headers: context.sessionHeaders
815
- });
792
+ ...input.shipping && { shipping_address: {
793
+ ...address,
794
+ first_name: input.shipping.firstName ?? "",
795
+ last_name: input.shipping.lastName ?? "",
796
+ address_1: input.shipping.address1 ?? "",
797
+ address_2: input.shipping.address2 ?? "",
798
+ company: input.shipping.company ?? "",
799
+ phone: input.shipping.phone ?? "",
800
+ city: input.shipping.city ?? "",
801
+ state: input.shipping.state ?? connInfo?.state ?? "",
802
+ country: input.shipping.country ?? connInfo?.country ?? "",
803
+ postcode: input.shipping.postcode ?? connInfo?.postcode ?? ""
804
+ } }
805
+ }, { headers: context.sessionHeaders });
816
806
  if (response.error) switch (response.error.code) {
817
807
  default:
818
808
  context.logger.error("Update cart customer unhandled error", response.error, { code: response.error.code });
@@ -829,14 +819,10 @@ const CART_ROUTER = {
829
819
  errors: SELECT_SHIPPING_RATE_ERROR_MAP,
830
820
  middlewares: [sessionMiddleware()]
831
821
  }, 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
- });
822
+ const response = await context.wordpress.woocommerce.store.cart.selectShippingRate({
823
+ rate_id: body.rateId,
824
+ package_id: body.packageId
825
+ }, { headers: context.sessionHeaders });
840
826
  if (response.error) switch (response.error.code) {
841
827
  case "woocommerce_rest_cart_shipping_rate_not_found": throw errors.CART_SHIPPING_RATE_NOT_FOUND({ message: response.error.message });
842
828
  case "woocommerce_rest_shipping_disabled": throw errors.CART_SHIPPING_DISABLED({ message: response.error.message });
@@ -856,15 +842,11 @@ const CART_ROUTER = {
856
842
  errors: ADD_CART_ITEM_ERROR_MAP,
857
843
  middlewares: [sessionMiddleware()]
858
844
  }, 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
- });
845
+ const response = await context.wordpress.woocommerce.store.cart.addItem({
846
+ id: input.productId,
847
+ quantity: input.quantity,
848
+ variation: input.variations ?? []
849
+ }, { headers: context.sessionHeaders });
868
850
  if (response.error) switch (response.error.code) {
869
851
  case "woocommerce_rest_product_out_of_stock": throw errors.CART_ITEM_OUT_OF_STOCK({ message: response.error.message });
870
852
  case "woocommerce_rest_product_partially_out_of_stock": throw errors.CART_ITEM_INSUFFICIENT_STOCK({ message: response.error.message });
@@ -893,14 +875,10 @@ const CART_ROUTER = {
893
875
  errors: UPDATE_CART_ITEM_ERROR_MAP,
894
876
  middlewares: [sessionMiddleware()]
895
877
  }, 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
- });
878
+ const response = await context.wordpress.woocommerce.store.cart.updateItem({
879
+ key: params.key,
880
+ quantity: body.quantity
881
+ }, { headers: context.sessionHeaders });
904
882
  if (response.error) switch (response.error.code) {
905
883
  case "woocommerce_rest_cart_invalid_key": throw errors.CART_ITEM_NOT_FOUND({ message: response.error.message });
906
884
  case "woocommerce_rest_cart_invalid_product": throw errors.CART_PRODUCT_INVALID({ message: response.error.message });
@@ -922,11 +900,7 @@ const CART_ROUTER = {
922
900
  errors: REMOVE_CART_ITEM_ERROR_MAP,
923
901
  middlewares: [sessionMiddleware()]
924
902
  }, 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
- });
903
+ const response = await context.wordpress.woocommerce.store.cart.removeItem({ key: params.key }, { headers: context.sessionHeaders });
930
904
  if (response.error) switch (response.error.code) {
931
905
  case "woocommerce_rest_cart_invalid_key": throw errors.CART_ITEM_NOT_FOUND({ message: response.error.message });
932
906
  default:
@@ -946,11 +920,7 @@ const CART_ROUTER = {
946
920
  errors: APPLY_COUPON_ERROR_MAP,
947
921
  middlewares: [sessionMiddleware()]
948
922
  }, 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
- });
923
+ const response = await context.wordpress.woocommerce.store.cart.applyCoupon({ code: body.code }, { headers: context.sessionHeaders });
954
924
  if (response.error) switch (response.error.code) {
955
925
  case "woocommerce_rest_cart_coupon_error": throw errors.CART_COUPON_INVALID({ message: response.error.message });
956
926
  case "woocommerce_rest_cart_coupon_disabled": throw errors.CART_COUPON_DISABLED({ message: response.error.message });
@@ -969,11 +939,7 @@ const CART_ROUTER = {
969
939
  errors: REMOVE_COUPON_ERROR_MAP,
970
940
  middlewares: [sessionMiddleware()]
971
941
  }, 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
- });
942
+ const response = await context.wordpress.woocommerce.store.cart.removeCoupon({ code: input.params.code }, { headers: context.sessionHeaders });
977
943
  if (response.error) switch (response.error.code) {
978
944
  case "woocommerce_rest_cart_coupon_error": throw errors.CART_COUPON_INVALID({ message: response.error.message });
979
945
  case "woocommerce_rest_cart_coupon_disabled": throw errors.CART_COUPON_DISABLED({ message: response.error.message });
@@ -1175,13 +1141,61 @@ const RetryCheckoutInput = z$2.object({
1175
1141
  orderId: NumberLike,
1176
1142
  paymentMethod: z$2.string(),
1177
1143
  billingEmail: z$2.email().optional(),
1178
- billingAddress: BillingAddress.optional(),
1144
+ billingAddress: BillingAddress,
1179
1145
  paymentData: CheckoutPaymentData.optional(),
1180
1146
  shippingAddress: ShippingAddress.optional()
1181
1147
  });
1182
1148
 
1183
1149
  //#endregion
1184
1150
  //#region src/checkout/utils.ts
1151
+ /**
1152
+ * A payment method the caller named, passed to WooCommerce as one.
1153
+ *
1154
+ * WooCommerce builds this argument's enum from `get_payment_gateway_ids()`, so the contract
1155
+ * enumerates the gateways enabled on the WordPress the client was generated against. That is true of
1156
+ * that install and says nothing about the one a build actually talks to, and WooCommerce agrees: its
1157
+ * own comment on the enum is that further validation happens during the request.
1158
+ *
1159
+ * So the enum is documentation here rather than a constraint, and the gateway a shopper chose is
1160
+ * sent whatever it is. WooCommerce rejects an unknown one with
1161
+ * `woocommerce_rest_checkout_payment_method_disabled`, which both callers already map.
1162
+ */
1163
+ function gateway(method) {
1164
+ return method;
1165
+ }
1166
+ /**
1167
+ * A Kizlo address in the shape WooCommerce registered its argument in.
1168
+ *
1169
+ * Kizlo's addresses are camelCase and WooCommerce's are snake_case, and until the contract described
1170
+ * the route nothing said so: the retry call passed a Kizlo address straight through as
1171
+ * `billing_address`, where every key missed and WooCommerce kept the address already on the order.
1172
+ *
1173
+ * Every field is required there, so an address given in part is sent filled out. An address that was
1174
+ * not given at all is a different thing and never reaches here: WooCommerce reads an absent
1175
+ * `shipping_address` as "use the billing address", and a blank one as an address, so a caller who
1176
+ * omitted it must leave the argument off the call rather than send this filled with empty strings.
1177
+ */
1178
+ function serializeAddress(address) {
1179
+ return {
1180
+ first_name: address.firstName,
1181
+ last_name: address.lastName,
1182
+ address_1: address.address1,
1183
+ address_2: address.address2 ?? "",
1184
+ company: address.company ?? "",
1185
+ city: address.city,
1186
+ state: address.state,
1187
+ country: address.country,
1188
+ postcode: address.postcode,
1189
+ phone: address.phone
1190
+ };
1191
+ }
1192
+ /** The same, plus the email WooCommerce carries on the billing address alone. */
1193
+ function serializeBillingAddress(address) {
1194
+ return {
1195
+ ...serializeAddress(address),
1196
+ email: address.email
1197
+ };
1198
+ }
1185
1199
  function deserializeCheckout(data) {
1186
1200
  return {
1187
1201
  cart: data.__experimentalCart ? deserializeCart(data.__experimentalCart) : null,
@@ -1210,16 +1224,40 @@ function deserializeCheckout(data) {
1210
1224
  postcode: data.billing_address.postcode,
1211
1225
  state: data.billing_address.state
1212
1226
  },
1213
- additionalFields: data.additional_fields,
1227
+ additionalFields: additionalFields(data.additional_fields),
1214
1228
  customerNote: data.customer_note,
1215
1229
  paymentMethod: data.payment_method,
1216
1230
  paymentResult: data.payment_result?.payment_status ? {
1217
- status: data.payment_result.payment_status,
1231
+ status: paymentStatus(data.payment_result.payment_status),
1218
1232
  redirectUrl: data.payment_result.redirect_url,
1219
1233
  data: data.payment_result.payment_details
1220
1234
  } : null
1221
1235
  };
1222
1236
  }
1237
+ /**
1238
+ * WooCommerce registers `additional_fields` as a bare object, so the contract describes it as one
1239
+ * with undescribed contents: what a site's checkout fields are is a per-site question that no schema
1240
+ * can answer ahead of time. The values are scalars in practice, and anything else is dropped rather
1241
+ * than passed on to fail the procedure's own output validation with a less useful message.
1242
+ */
1243
+ function additionalFields(fields) {
1244
+ const kept = {};
1245
+ for (const [key, value] of Object.entries(fields ?? {})) if (typeof value === "string" || typeof value === "boolean") kept[key] = value;
1246
+ return kept;
1247
+ }
1248
+ /**
1249
+ * WooCommerce names the four statuses in the field's description and declares the field a plain
1250
+ * string, so the contract cannot narrow it. `error` is the safe reading of anything unrecognized: a
1251
+ * gateway that answered with something new has not taken payment as far as this checkout knows.
1252
+ */
1253
+ function paymentStatus(status) {
1254
+ switch (status) {
1255
+ case "success":
1256
+ case "pending":
1257
+ case "failure": return status;
1258
+ default: return "error";
1259
+ }
1260
+ }
1223
1261
 
1224
1262
  //#endregion
1225
1263
  //#region src/checkout/index.ts
@@ -1232,10 +1270,7 @@ const CHECKOUT_ROUTER = {
1232
1270
  errors: GET_CHECKOUT_ERROR_MAP,
1233
1271
  middlewares: [sessionMiddleware()]
1234
1272
  }, async ({ context, errors }) => {
1235
- const response = await context.wordpress.get("/checkout", {
1236
- base: WC_STORE_BASE,
1237
- headers: context.sessionHeaders
1238
- });
1273
+ const response = await context.wordpress.woocommerce.store.checkout.get({}, { headers: context.sessionHeaders });
1239
1274
  if (response.error) switch (response.error.code) {
1240
1275
  case "woocommerce_rest_checkout_missing_order": throw errors.CHECKOUT_ORDER_NOT_FOUND({ message: response.error.message });
1241
1276
  default:
@@ -1253,16 +1288,12 @@ const CHECKOUT_ROUTER = {
1253
1288
  errors: UPDATE_CHECKOUT_ERROR_MAP,
1254
1289
  middlewares: [sessionMiddleware()]
1255
1290
  }, 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
- });
1291
+ const response = await context.wordpress.woocommerce.store.checkout.update({
1292
+ order_notes: input.body.customerNote,
1293
+ payment_method: gateway(input.body.paymentMethod),
1294
+ additional_fields: input.body.additionalFields,
1295
+ __experimental_calc_totals: input.body.recalculateTotals
1296
+ }, { headers: context.sessionHeaders });
1266
1297
  if (response.error) switch (response.error.code) {
1267
1298
  case "woocommerce_rest_cart_coupon_error": throw errors.CHECKOUT_COUPON_INVALID({ message: response.error.message });
1268
1299
  case "woocommerce_rest_checkout_payment_method_disabled": throw errors.CHECKOUT_PAYMENT_METHOD_DISABLED({ message: response.error.message });
@@ -1287,30 +1318,23 @@ const CHECKOUT_ROUTER = {
1287
1318
  errors: CONFIRM_CHECKOUT_ERROR_MAP,
1288
1319
  middlewares: [sessionMiddleware()]
1289
1320
  }, async ({ context, input, errors }) => {
1290
- const checkoutResponse = await context.wordpress.get("/checkout", {
1291
- base: WC_STORE_BASE,
1292
- headers: context.sessionHeaders
1293
- });
1321
+ const checkoutResponse = await context.wordpress.woocommerce.store.checkout.get({}, { headers: context.sessionHeaders });
1294
1322
  if (checkoutResponse.error) switch (checkoutResponse.error.code) {
1295
1323
  case "woocommerce_rest_checkout_missing_order": throw errors.CHECKOUT_ORDER_NOT_FOUND({ message: checkoutResponse.error.message });
1296
1324
  default:
1297
1325
  context.logger.error("Get checkout for confirm unhandled error", checkoutResponse.error, { code: checkoutResponse.error.code });
1298
1326
  throw errors.INTERNAL_SERVER_ERROR();
1299
1327
  }
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
- });
1328
+ const confirmResponse = await context.wordpress.woocommerce.store.checkout.process({
1329
+ payment_data: input.body.paymentData,
1330
+ customer_password: input.body.customerPassword,
1331
+ customer_note: checkoutResponse.data.customer_note,
1332
+ payment_method: checkoutResponse.data.payment_method,
1333
+ create_account: !!input.body.customerPassword?.length,
1334
+ billing_address: checkoutResponse.data.billing_address,
1335
+ shipping_address: checkoutResponse.data.shipping_address,
1336
+ additional_fields: checkoutResponse.data.additional_fields
1337
+ }, { headers: context.sessionHeaders });
1314
1338
  if (confirmResponse.error) switch (confirmResponse.error.code) {
1315
1339
  case "woocommerce_rest_invalid_address": throw errors.CHECKOUT_ADDRESS_INVALID({ message: confirmResponse.error.message });
1316
1340
  case "woocommerce_rest_invalid_address_country": throw errors.CHECKOUT_ADDRESS_COUNTRY_INVALID({ message: confirmResponse.error.message });
@@ -1346,19 +1370,15 @@ const CHECKOUT_ROUTER = {
1346
1370
  errors: RETRY_CHECKOUT_ERROR_MAP,
1347
1371
  middlewares: [sessionMiddleware()]
1348
1372
  }, 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
- });
1373
+ const response = await context.wordpress.woocommerce.store.checkout.processOrder({
1374
+ key: input.body.key,
1375
+ id: input.params.orderId,
1376
+ payment_data: input.body.paymentData,
1377
+ billing_email: input.body.billingEmail,
1378
+ payment_method: gateway(input.body.paymentMethod),
1379
+ billing_address: serializeBillingAddress(input.body.billingAddress),
1380
+ shipping_address: input.body.shippingAddress ? serializeAddress(input.body.shippingAddress) : void 0
1381
+ }, { headers: context.sessionHeaders });
1362
1382
  if (response.error) switch (response.error.code) {
1363
1383
  case "woocommerce_rest_invalid_billing_email": throw errors.CHECKOUT_EMAIL_INVALID({ message: response.error.message });
1364
1384
  case "woocommerce_rest_checkout_process_payment_error": throw errors.CHECKOUT_PAYMENT_FAILED({ message: response.error.message });
@@ -1447,8 +1467,17 @@ const CUSTOMER_ROUTER = { get: createProcedure({
1447
1467
  }, async ({ context, errors }) => {
1448
1468
  const auth = await context.getAuthUser();
1449
1469
  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;
1470
+ const response = await context.wordpress.woocommerce.customers.retrieve({ id: auth.id });
1471
+ if (response.error) switch (response.error.code) {
1472
+ case "wc_user_invalid_id": throw errors.NOT_FOUND();
1473
+ case "woocommerce_rest_cannot_view": throw errors.FORBIDDEN();
1474
+ default:
1475
+ context.logger.error("Get customer unhandled error", response.error, {
1476
+ userId: auth.id,
1477
+ code: response.error.code
1478
+ });
1479
+ throw errors.INTERNAL_SERVER_ERROR();
1480
+ }
1452
1481
  return deserializeCustomer(response.data);
1453
1482
  }) };
1454
1483
 
@@ -1472,13 +1501,13 @@ function deserializeProduct(data) {
1472
1501
  description: data.description,
1473
1502
  shortDescription: data.short_description,
1474
1503
  prices: {
1475
- price: +data.kizlo.prices.price,
1476
- salePrice: +data.kizlo.prices.sale_price,
1477
- regularPrice: +data.kizlo.prices.regular_price
1504
+ price: data.kizlo.prices.price,
1505
+ salePrice: data.kizlo.prices.sale_price,
1506
+ regularPrice: data.kizlo.prices.regular_price
1478
1507
  },
1479
1508
  isSoldIndividually: data.sold_individually,
1480
1509
  onSaleFrom: data.date_on_sale_from ? toTimestamp(data.date_on_sale_from) : null,
1481
- lowStockRemaining: "data.low_stock_amount",
1510
+ lowStockRemaining: data.low_stock_amount,
1482
1511
  onSaleTo: data.date_on_sale_to ? toTimestamp(data.date_on_sale_to) : null,
1483
1512
  isInStock: data.stock_status === "instock",
1484
1513
  stock: data.stock_quantity,
@@ -1508,9 +1537,15 @@ function deserializeProduct(data) {
1508
1537
  };
1509
1538
  }
1510
1539
  function deserializeStoreProduct(data) {
1540
+ const kizlo = data.extensions.kizlo ?? {
1541
+ stock: null,
1542
+ on_sale_from: null,
1543
+ on_sale_to: null,
1544
+ hs_code: null
1545
+ };
1511
1546
  return {
1512
1547
  id: data.id,
1513
- type: data.type,
1548
+ type: productType(data.type),
1514
1549
  name: data.name,
1515
1550
  slug: data.slug,
1516
1551
  sku: data.sku,
@@ -1545,9 +1580,9 @@ function deserializeStoreProduct(data) {
1545
1580
  isOnSale: data.on_sale,
1546
1581
  parentId: data.parent,
1547
1582
  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,
1583
+ stock: kizlo.stock,
1584
+ onSaleFrom: kizlo.on_sale_from ? toTimestamp(kizlo.on_sale_from) : null,
1585
+ onSaleTo: kizlo.on_sale_to ? toTimestamp(kizlo.on_sale_to) : null,
1551
1586
  seo: null,
1552
1587
  meta: {}
1553
1588
  };
@@ -1557,7 +1592,13 @@ function deserializeProductFilters(data) {
1557
1592
  const maxPrice = +data.price_range.max_price;
1558
1593
  const minPrice = +data.price_range.min_price;
1559
1594
  return {
1560
- stockStatuses: data.stock_status_counts ?? [],
1595
+ stockStatuses: (data.stock_status_counts ?? []).flatMap((entry) => {
1596
+ const status = stockStatus(entry.status);
1597
+ return status ? [{
1598
+ count: entry.count,
1599
+ status
1600
+ }] : [];
1601
+ }),
1561
1602
  taxonomyTerms: data.kizlo.taxonomy_counts.map((item) => ({
1562
1603
  id: item.id,
1563
1604
  name: item.name,
@@ -1608,7 +1649,7 @@ function serializeProductListInput(data) {
1608
1649
  orderby: data?.orderby,
1609
1650
  parent: normalizeArrayableValue(data?.parent),
1610
1651
  parent_exclude: normalizeArrayableValue(data?.parentExclude),
1611
- rating: normalizeArrayableValue(data?.rating)?.map(Number),
1652
+ rating: normalizeArrayableValue(data?.rating)?.map((value) => Number(value)),
1612
1653
  sku: data?.sku,
1613
1654
  slug: data?.slug,
1614
1655
  stock_status: normalizeArrayableValue(data?.stockStatus),
@@ -1631,6 +1672,20 @@ function serializeProductListInput(data) {
1631
1672
  search: data?.search
1632
1673
  };
1633
1674
  }
1675
+ /**
1676
+ * WooCommerce declares both of these fields as plain strings, so the contract cannot narrow them and
1677
+ * neither can this without saying what it does with a value it does not recognize.
1678
+ *
1679
+ * A product type Kizlo has no name for reads as `simple`, which is what every gateway to a product
1680
+ * page needs it to behave like. An unrecognized stock status is dropped from the filter list
1681
+ * instead: a count nobody can label is not a filter anyone can offer.
1682
+ */
1683
+ function productType(type) {
1684
+ return PRODUCT_TYPES.includes(type) ? type : "simple";
1685
+ }
1686
+ function stockStatus(status) {
1687
+ return PRODUCT_STOCK_STATUSES.includes(status) ? status : null;
1688
+ }
1634
1689
  function deserializeImages(images) {
1635
1690
  return images.map((item) => ({
1636
1691
  id: item.id,
@@ -1665,7 +1720,7 @@ const PRODUCT_ROUTER = {
1665
1720
  if (input.query?.previewToken) {
1666
1721
  const result = await context.verifyPreviewToken(input.query.previewToken);
1667
1722
  if (!result) throw errors.PRODUCT_NOT_FOUND();
1668
- const response$1 = await context.wordpress.get(`/products/${result.id}`, { base: WC_CORE_BASE });
1723
+ const response$1 = await context.wordpress.woocommerce.products.retrieve({ id: Number(result.id) });
1669
1724
  if (response$1.error) switch (response$1.error.code) {
1670
1725
  case "woocommerce_rest_product_invalid_id": throw errors.PRODUCT_NOT_FOUND({ message: response$1.error.message });
1671
1726
  default:
@@ -1677,10 +1732,7 @@ const PRODUCT_ROUTER = {
1677
1732
  }
1678
1733
  return deserializeProduct(response$1.data);
1679
1734
  }
1680
- const response = await context.wordpress.get("/products", {
1681
- searchParams: { slug: input.params.identifier },
1682
- base: WC_CORE_BASE
1683
- });
1735
+ const response = await context.wordpress.woocommerce.products.list({ slug: String(input.params.identifier) });
1684
1736
  if (response.error) switch (response.error.code) {
1685
1737
  default:
1686
1738
  context.logger.error("Get product unhandled error", response.error, {
@@ -1702,10 +1754,7 @@ const PRODUCT_ROUTER = {
1702
1754
  errors: LIST_PRODUCT_ERROR_MAP
1703
1755
  }, async ({ context, input, errors }) => {
1704
1756
  const searchParams = serializeProductListInput(input.query);
1705
- const response = await context.wordpress.get("/products", {
1706
- base: WC_STORE_BASE,
1707
- searchParams: { ...searchParams }
1708
- });
1757
+ const response = await context.wordpress.woocommerce.store.products.list(searchParams);
1709
1758
  if (response.error) switch (response.error.code) {
1710
1759
  default:
1711
1760
  context.logger.error("List products unhandled error", response.error, { code: response.error.code });
@@ -1729,19 +1778,16 @@ const PRODUCT_ROUTER = {
1729
1778
  output: ProductFilters.nullable()
1730
1779
  }, async ({ context, errors, input }) => {
1731
1780
  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
- }
1781
+ const response = await context.wordpress.woocommerce.store.products.collectionData({
1782
+ ...searchParams,
1783
+ calculate_price_range: true,
1784
+ calculate_rating_counts: input.query?.ratingFilters,
1785
+ calculate_taxonomy_counts: input.query?.taxonomyFilters,
1786
+ calculate_stock_status_counts: input.query?.stockStatusFilters,
1787
+ calculate_attribute_counts: input.query?.attributeFilters?.map((item) => ({
1788
+ taxonomy: item.taxonomy,
1789
+ query_type: item.queryType
1790
+ }))
1745
1791
  });
1746
1792
  if (response.error) switch (response.error.code) {
1747
1793
  default:
@@ -1757,6 +1803,21 @@ const PRODUCT_ROUTER = {
1757
1803
  function woocommerce() {
1758
1804
  return createExtension({
1759
1805
  id: "woocommerce",
1806
+ requires: {
1807
+ plugin: {
1808
+ slug: "kizlo-woocommerce",
1809
+ name: "Kizlo WooCommerce",
1810
+ version: "0.2.0"
1811
+ },
1812
+ endpoints: [
1813
+ "woocommerce.customers",
1814
+ "woocommerce.products",
1815
+ "woocommerce.kizlo.cart",
1816
+ "woocommerce.store.cart",
1817
+ "woocommerce.store.checkout",
1818
+ "woocommerce.store.products"
1819
+ ]
1820
+ },
1760
1821
  init: () => {
1761
1822
  return { router: {
1762
1823
  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.0",
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
  }