@bacano/sdk 1.2.6 → 1.2.8

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/react.cjs CHANGED
@@ -5930,6 +5930,78 @@ function hasRecoverableCartSessionError(errors) {
5930
5930
  );
5931
5931
  }
5932
5932
 
5933
+ // libs/sdk/src/modules/checkout-contact.ts
5934
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5935
+ var PHONE_PATTERN = /^\+?\d{7,15}$/;
5936
+ function validEmail(value) {
5937
+ if (value.length > 320 || !EMAIL_PATTERN.test(value)) return false;
5938
+ const [local, domain] = value.split("@");
5939
+ if (!local || !domain || local.length > 64 || domain.length > 255 || local.startsWith(".") || local.endsWith(".") || local.includes("..")) {
5940
+ return false;
5941
+ }
5942
+ return domain.split(".").every((label) => /^[a-z\d](?:[a-z\d-]*[a-z\d])?$/i.test(label));
5943
+ }
5944
+ function normalizePhone(value) {
5945
+ return value.trim().replace(/[\s().-]/g, "");
5946
+ }
5947
+ function invalidContact(field) {
5948
+ const messages = {
5949
+ email: "Revisa el correo electr\xF3nico: debe estar completo y sin espacios.",
5950
+ phone: "Revisa el n\xFAmero de tel\xE9fono: usa entre 7 y 15 d\xEDgitos.",
5951
+ contact: "Ingresa un correo electr\xF3nico o un n\xFAmero de tel\xE9fono v\xE1lido."
5952
+ };
5953
+ return new WebsiteError(
5954
+ messages[field],
5955
+ `INVALID_CHECKOUT_${field.toUpperCase()}`
5956
+ );
5957
+ }
5958
+ function normalizeCheckoutContact(contact, mode) {
5959
+ const email = contact.email?.trim() || void 0;
5960
+ const phone = contact.phone ? normalizePhone(contact.phone) : void 0;
5961
+ const cellphone = contact.cellphone ? normalizePhone(contact.cellphone) : void 0;
5962
+ const emailValid = !email || validEmail(email);
5963
+ const phoneValid = !phone || PHONE_PATTERN.test(phone);
5964
+ const cellphoneValid = !cellphone || PHONE_PATTERN.test(cellphone);
5965
+ if (mode === "submit") {
5966
+ if (!emailValid) throw invalidContact("email");
5967
+ if (!phoneValid || !cellphoneValid) throw invalidContact("phone");
5968
+ }
5969
+ const normalized = {
5970
+ ...contact,
5971
+ name: contact.name?.trim() || void 0,
5972
+ firstName: contact.firstName?.trim() || void 0,
5973
+ lastName: contact.lastName?.trim() || void 0,
5974
+ email: emailValid ? email : void 0,
5975
+ phone: phoneValid ? phone : void 0,
5976
+ cellphone: cellphoneValid ? cellphone : void 0
5977
+ };
5978
+ if ((normalized.name?.length ?? 0) > 160 || (normalized.firstName?.length ?? 0) > 80 || (normalized.lastName?.length ?? 0) > 80) {
5979
+ throw new WebsiteError(
5980
+ "Revisa los datos de contacto: el nombre es demasiado largo.",
5981
+ "INVALID_CHECKOUT_CONTACT"
5982
+ );
5983
+ }
5984
+ if (!normalized.email && !normalized.phone && !normalized.cellphone) {
5985
+ throw invalidContact(
5986
+ !emailValid ? "email" : !phoneValid || !cellphoneValid ? "phone" : "contact"
5987
+ );
5988
+ }
5989
+ return normalized;
5990
+ }
5991
+ function explainCheckoutRequestError(error) {
5992
+ if (error instanceof GraphQLRequestError && error.graphqlErrors.some(
5993
+ (item) => /invalid request payload|hay un error en los datos del pedido/i.test(
5994
+ item.message
5995
+ )
5996
+ )) {
5997
+ throw new WebsiteError(
5998
+ "Hay un error en los datos del pedido. Rev\xEDsalos e intenta de nuevo.",
5999
+ "INVALID_CHECKOUT_DATA"
6000
+ );
6001
+ }
6002
+ throw error;
6003
+ }
6004
+
5933
6005
  // libs/sdk/src/modules/cart.ts
5934
6006
  function mapCartItem(raw) {
5935
6007
  return {
@@ -6005,6 +6077,15 @@ function requireCartSession(storage) {
6005
6077
  return session;
6006
6078
  }
6007
6079
  function createCartModule(graphqlUrl, getHeaders, getContext, cartSessionStorage) {
6080
+ let pendingCartSessionOperation = Promise.resolve();
6081
+ function enqueueCartSessionOperation(run) {
6082
+ const operation = pendingCartSessionOperation.then(run, run);
6083
+ pendingCartSessionOperation = operation.then(
6084
+ () => void 0,
6085
+ () => void 0
6086
+ );
6087
+ return operation;
6088
+ }
6008
6089
  async function fetchCart(orderId, cartToken) {
6009
6090
  const data = await executeGraphQL(graphqlUrl, GET_CART, { orderId, cartToken }, getHeaders);
6010
6091
  const result = data.websiteGetCart;
@@ -6027,72 +6108,80 @@ function createCartModule(graphqlUrl, getHeaders, getContext, cartSessionStorage
6027
6108
  }
6028
6109
  return {
6029
6110
  async getOrCreate(contact, fulfillmentFamily) {
6030
- const ctx = getContext();
6031
- if (!ctx.chainId) {
6032
- throw new WebsiteError(
6033
- "This website has no chain configured. Cart requires a chain_id.",
6034
- "NO_CHAIN"
6035
- );
6036
- }
6037
- const existing = cartSessionStorage.get();
6038
- const data = await executeGraphQL(
6039
- graphqlUrl,
6040
- CREATE_CART,
6041
- {
6042
- websiteId: ctx.websiteId,
6043
- cartToken: existing?.cartToken,
6044
- contact,
6045
- fulfillmentFamily
6046
- },
6047
- getHeaders
6048
- );
6049
- const result = data.websiteCreateCart;
6050
- assertActionSuccess(result, "CART_CREATE_ERROR");
6051
- cartSessionStorage.set({
6052
- orderId: result.order_id,
6053
- cartToken: result.cart_token
6054
- });
6055
- return mapCart(result.cart, result.cart_token);
6056
- },
6057
- async startCheckout(input) {
6058
- const ctx = getContext();
6059
- if (!ctx.chainId) {
6060
- throw new WebsiteError(
6061
- "This website has no chain configured. Checkout requires a chain_id.",
6062
- "NO_CHAIN"
6063
- );
6064
- }
6065
- const requestStartCheckout = async (cartToken) => {
6111
+ return enqueueCartSessionOperation(async () => {
6112
+ const normalizedContact = contact ? normalizeCheckoutContact(contact, "draft") : void 0;
6113
+ const ctx = getContext();
6114
+ if (!ctx.chainId) {
6115
+ throw new WebsiteError(
6116
+ "This website has no chain configured. Cart requires a chain_id.",
6117
+ "NO_CHAIN"
6118
+ );
6119
+ }
6120
+ const existing = cartSessionStorage.get();
6066
6121
  const data = await executeGraphQL(
6067
6122
  graphqlUrl,
6068
- START_GUEST_CHECKOUT,
6123
+ CREATE_CART,
6069
6124
  {
6070
6125
  websiteId: ctx.websiteId,
6071
- cartToken,
6072
- contact: input.contact,
6073
- fulfillmentFamily: input.fulfillmentFamily,
6074
- items: input.items.map((item) => ({
6075
- product_variant_id: item.productVariantId,
6076
- quantity: item.quantity
6077
- })),
6078
- marketingAttribution: input.marketingAttribution ? serializeWebsiteMarketingAttribution(input.marketingAttribution) : void 0
6126
+ cartToken: existing?.cartToken,
6127
+ contact: normalizedContact,
6128
+ fulfillmentFamily
6079
6129
  },
6080
6130
  getHeaders
6081
- );
6082
- return data.websiteStartGuestCheckout;
6083
- };
6084
- const existing = cartSessionStorage.get();
6085
- let result = await requestStartCheckout(existing?.cartToken);
6086
- if (existing && !result.success && hasRecoverableCartSessionError(result.errors)) {
6087
- cartSessionStorage.clear();
6088
- result = await requestStartCheckout();
6089
- }
6090
- assertActionSuccess(result, "CHECKOUT_START_ERROR");
6091
- cartSessionStorage.set({
6092
- orderId: result.order_id,
6093
- cartToken: result.cart_token
6131
+ ).catch(explainCheckoutRequestError);
6132
+ const result = data.websiteCreateCart;
6133
+ assertActionSuccess(result, "CART_CREATE_ERROR");
6134
+ cartSessionStorage.set({
6135
+ orderId: result.order_id,
6136
+ cartToken: result.cart_token
6137
+ });
6138
+ return mapCart(result.cart, result.cart_token);
6139
+ });
6140
+ },
6141
+ async startCheckout(input) {
6142
+ return enqueueCartSessionOperation(async () => {
6143
+ const contact = normalizeCheckoutContact(input.contact, "draft");
6144
+ const ctx = getContext();
6145
+ if (!ctx.chainId) {
6146
+ throw new WebsiteError(
6147
+ "This website has no chain configured. Checkout requires a chain_id.",
6148
+ "NO_CHAIN"
6149
+ );
6150
+ }
6151
+ const requestStartCheckout = async (cartToken) => {
6152
+ const data = await executeGraphQL(
6153
+ graphqlUrl,
6154
+ START_GUEST_CHECKOUT,
6155
+ {
6156
+ websiteId: ctx.websiteId,
6157
+ cartToken,
6158
+ contact,
6159
+ fulfillmentFamily: input.fulfillmentFamily,
6160
+ items: input.items.map((item) => ({
6161
+ product_variant_id: item.productVariantId,
6162
+ quantity: item.quantity
6163
+ })),
6164
+ marketingAttribution: input.marketingAttribution ? serializeWebsiteMarketingAttribution(
6165
+ input.marketingAttribution
6166
+ ) : void 0
6167
+ },
6168
+ getHeaders
6169
+ ).catch(explainCheckoutRequestError);
6170
+ return data.websiteStartGuestCheckout;
6171
+ };
6172
+ const existing = cartSessionStorage.get();
6173
+ let result = await requestStartCheckout(existing?.cartToken);
6174
+ if (existing && !result.success && hasRecoverableCartSessionError(result.errors)) {
6175
+ cartSessionStorage.clear();
6176
+ result = await requestStartCheckout();
6177
+ }
6178
+ assertActionSuccess(result, "CHECKOUT_START_ERROR");
6179
+ cartSessionStorage.set({
6180
+ orderId: result.order_id,
6181
+ cartToken: result.cart_token
6182
+ });
6183
+ return mapCart(result.cart, result.cart_token);
6094
6184
  });
6095
- return mapCart(result.cart, result.cart_token);
6096
6185
  },
6097
6186
  async get() {
6098
6187
  const session = cartSessionStorage.get();
@@ -6631,6 +6720,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
6631
6720
  return mapCouponResult(data.websiteRemoveCoupon);
6632
6721
  },
6633
6722
  async submit(input) {
6723
+ const contact = normalizeCheckoutContact(input.contact, "submit");
6634
6724
  const session = requireCartSession2(cartSessionStorage);
6635
6725
  const data = await executeGraphQL(
6636
6726
  graphqlUrl,
@@ -6638,7 +6728,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
6638
6728
  {
6639
6729
  orderId: session.orderId,
6640
6730
  cartToken: session.cartToken,
6641
- contact: input.contact,
6731
+ contact,
6642
6732
  fulfillmentFamily: input.fulfillmentFamily,
6643
6733
  address: input.address,
6644
6734
  shippingQuoteId: input.shippingQuoteId,
@@ -6658,7 +6748,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
6658
6748
  } : void 0
6659
6749
  },
6660
6750
  getHeaders
6661
- );
6751
+ ).catch(explainCheckoutRequestError);
6662
6752
  const result = data.websiteCheckout;
6663
6753
  if (!result.success && result.errors && result.errors.length > 0) {
6664
6754
  throw new CheckoutError(result.errors);
@@ -6699,7 +6789,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
6699
6789
  returnUrl: input.returnUrl
6700
6790
  },
6701
6791
  getHeaders
6702
- );
6792
+ ).catch(explainCheckoutRequestError);
6703
6793
  const result = data.websiteCreatePaymentSession;
6704
6794
  if (result.success && result.payment_session_id && (result.integration_mode !== "redirect" || !result.redirect_url)) {
6705
6795
  throw new WebsiteError(
@@ -6734,7 +6824,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
6734
6824
  gatewayTransactionId: input.gatewayTransactionId
6735
6825
  },
6736
6826
  getHeaders
6737
- );
6827
+ ).catch(explainCheckoutRequestError);
6738
6828
  const result = data.websiteReconcilePaymentSession;
6739
6829
  return {
6740
6830
  success: result.success,
package/react.js CHANGED
@@ -5877,6 +5877,78 @@ function hasRecoverableCartSessionError(errors) {
5877
5877
  );
5878
5878
  }
5879
5879
 
5880
+ // libs/sdk/src/modules/checkout-contact.ts
5881
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5882
+ var PHONE_PATTERN = /^\+?\d{7,15}$/;
5883
+ function validEmail(value) {
5884
+ if (value.length > 320 || !EMAIL_PATTERN.test(value)) return false;
5885
+ const [local, domain] = value.split("@");
5886
+ if (!local || !domain || local.length > 64 || domain.length > 255 || local.startsWith(".") || local.endsWith(".") || local.includes("..")) {
5887
+ return false;
5888
+ }
5889
+ return domain.split(".").every((label) => /^[a-z\d](?:[a-z\d-]*[a-z\d])?$/i.test(label));
5890
+ }
5891
+ function normalizePhone(value) {
5892
+ return value.trim().replace(/[\s().-]/g, "");
5893
+ }
5894
+ function invalidContact(field) {
5895
+ const messages = {
5896
+ email: "Revisa el correo electr\xF3nico: debe estar completo y sin espacios.",
5897
+ phone: "Revisa el n\xFAmero de tel\xE9fono: usa entre 7 y 15 d\xEDgitos.",
5898
+ contact: "Ingresa un correo electr\xF3nico o un n\xFAmero de tel\xE9fono v\xE1lido."
5899
+ };
5900
+ return new WebsiteError(
5901
+ messages[field],
5902
+ `INVALID_CHECKOUT_${field.toUpperCase()}`
5903
+ );
5904
+ }
5905
+ function normalizeCheckoutContact(contact, mode) {
5906
+ const email = contact.email?.trim() || void 0;
5907
+ const phone = contact.phone ? normalizePhone(contact.phone) : void 0;
5908
+ const cellphone = contact.cellphone ? normalizePhone(contact.cellphone) : void 0;
5909
+ const emailValid = !email || validEmail(email);
5910
+ const phoneValid = !phone || PHONE_PATTERN.test(phone);
5911
+ const cellphoneValid = !cellphone || PHONE_PATTERN.test(cellphone);
5912
+ if (mode === "submit") {
5913
+ if (!emailValid) throw invalidContact("email");
5914
+ if (!phoneValid || !cellphoneValid) throw invalidContact("phone");
5915
+ }
5916
+ const normalized = {
5917
+ ...contact,
5918
+ name: contact.name?.trim() || void 0,
5919
+ firstName: contact.firstName?.trim() || void 0,
5920
+ lastName: contact.lastName?.trim() || void 0,
5921
+ email: emailValid ? email : void 0,
5922
+ phone: phoneValid ? phone : void 0,
5923
+ cellphone: cellphoneValid ? cellphone : void 0
5924
+ };
5925
+ if ((normalized.name?.length ?? 0) > 160 || (normalized.firstName?.length ?? 0) > 80 || (normalized.lastName?.length ?? 0) > 80) {
5926
+ throw new WebsiteError(
5927
+ "Revisa los datos de contacto: el nombre es demasiado largo.",
5928
+ "INVALID_CHECKOUT_CONTACT"
5929
+ );
5930
+ }
5931
+ if (!normalized.email && !normalized.phone && !normalized.cellphone) {
5932
+ throw invalidContact(
5933
+ !emailValid ? "email" : !phoneValid || !cellphoneValid ? "phone" : "contact"
5934
+ );
5935
+ }
5936
+ return normalized;
5937
+ }
5938
+ function explainCheckoutRequestError(error) {
5939
+ if (error instanceof GraphQLRequestError && error.graphqlErrors.some(
5940
+ (item) => /invalid request payload|hay un error en los datos del pedido/i.test(
5941
+ item.message
5942
+ )
5943
+ )) {
5944
+ throw new WebsiteError(
5945
+ "Hay un error en los datos del pedido. Rev\xEDsalos e intenta de nuevo.",
5946
+ "INVALID_CHECKOUT_DATA"
5947
+ );
5948
+ }
5949
+ throw error;
5950
+ }
5951
+
5880
5952
  // libs/sdk/src/modules/cart.ts
5881
5953
  function mapCartItem(raw) {
5882
5954
  return {
@@ -5952,6 +6024,15 @@ function requireCartSession(storage) {
5952
6024
  return session;
5953
6025
  }
5954
6026
  function createCartModule(graphqlUrl, getHeaders, getContext, cartSessionStorage) {
6027
+ let pendingCartSessionOperation = Promise.resolve();
6028
+ function enqueueCartSessionOperation(run) {
6029
+ const operation = pendingCartSessionOperation.then(run, run);
6030
+ pendingCartSessionOperation = operation.then(
6031
+ () => void 0,
6032
+ () => void 0
6033
+ );
6034
+ return operation;
6035
+ }
5955
6036
  async function fetchCart(orderId, cartToken) {
5956
6037
  const data = await executeGraphQL(graphqlUrl, GET_CART, { orderId, cartToken }, getHeaders);
5957
6038
  const result = data.websiteGetCart;
@@ -5974,72 +6055,80 @@ function createCartModule(graphqlUrl, getHeaders, getContext, cartSessionStorage
5974
6055
  }
5975
6056
  return {
5976
6057
  async getOrCreate(contact, fulfillmentFamily) {
5977
- const ctx = getContext();
5978
- if (!ctx.chainId) {
5979
- throw new WebsiteError(
5980
- "This website has no chain configured. Cart requires a chain_id.",
5981
- "NO_CHAIN"
5982
- );
5983
- }
5984
- const existing = cartSessionStorage.get();
5985
- const data = await executeGraphQL(
5986
- graphqlUrl,
5987
- CREATE_CART,
5988
- {
5989
- websiteId: ctx.websiteId,
5990
- cartToken: existing?.cartToken,
5991
- contact,
5992
- fulfillmentFamily
5993
- },
5994
- getHeaders
5995
- );
5996
- const result = data.websiteCreateCart;
5997
- assertActionSuccess(result, "CART_CREATE_ERROR");
5998
- cartSessionStorage.set({
5999
- orderId: result.order_id,
6000
- cartToken: result.cart_token
6001
- });
6002
- return mapCart(result.cart, result.cart_token);
6003
- },
6004
- async startCheckout(input) {
6005
- const ctx = getContext();
6006
- if (!ctx.chainId) {
6007
- throw new WebsiteError(
6008
- "This website has no chain configured. Checkout requires a chain_id.",
6009
- "NO_CHAIN"
6010
- );
6011
- }
6012
- const requestStartCheckout = async (cartToken) => {
6058
+ return enqueueCartSessionOperation(async () => {
6059
+ const normalizedContact = contact ? normalizeCheckoutContact(contact, "draft") : void 0;
6060
+ const ctx = getContext();
6061
+ if (!ctx.chainId) {
6062
+ throw new WebsiteError(
6063
+ "This website has no chain configured. Cart requires a chain_id.",
6064
+ "NO_CHAIN"
6065
+ );
6066
+ }
6067
+ const existing = cartSessionStorage.get();
6013
6068
  const data = await executeGraphQL(
6014
6069
  graphqlUrl,
6015
- START_GUEST_CHECKOUT,
6070
+ CREATE_CART,
6016
6071
  {
6017
6072
  websiteId: ctx.websiteId,
6018
- cartToken,
6019
- contact: input.contact,
6020
- fulfillmentFamily: input.fulfillmentFamily,
6021
- items: input.items.map((item) => ({
6022
- product_variant_id: item.productVariantId,
6023
- quantity: item.quantity
6024
- })),
6025
- marketingAttribution: input.marketingAttribution ? serializeWebsiteMarketingAttribution(input.marketingAttribution) : void 0
6073
+ cartToken: existing?.cartToken,
6074
+ contact: normalizedContact,
6075
+ fulfillmentFamily
6026
6076
  },
6027
6077
  getHeaders
6028
- );
6029
- return data.websiteStartGuestCheckout;
6030
- };
6031
- const existing = cartSessionStorage.get();
6032
- let result = await requestStartCheckout(existing?.cartToken);
6033
- if (existing && !result.success && hasRecoverableCartSessionError(result.errors)) {
6034
- cartSessionStorage.clear();
6035
- result = await requestStartCheckout();
6036
- }
6037
- assertActionSuccess(result, "CHECKOUT_START_ERROR");
6038
- cartSessionStorage.set({
6039
- orderId: result.order_id,
6040
- cartToken: result.cart_token
6078
+ ).catch(explainCheckoutRequestError);
6079
+ const result = data.websiteCreateCart;
6080
+ assertActionSuccess(result, "CART_CREATE_ERROR");
6081
+ cartSessionStorage.set({
6082
+ orderId: result.order_id,
6083
+ cartToken: result.cart_token
6084
+ });
6085
+ return mapCart(result.cart, result.cart_token);
6086
+ });
6087
+ },
6088
+ async startCheckout(input) {
6089
+ return enqueueCartSessionOperation(async () => {
6090
+ const contact = normalizeCheckoutContact(input.contact, "draft");
6091
+ const ctx = getContext();
6092
+ if (!ctx.chainId) {
6093
+ throw new WebsiteError(
6094
+ "This website has no chain configured. Checkout requires a chain_id.",
6095
+ "NO_CHAIN"
6096
+ );
6097
+ }
6098
+ const requestStartCheckout = async (cartToken) => {
6099
+ const data = await executeGraphQL(
6100
+ graphqlUrl,
6101
+ START_GUEST_CHECKOUT,
6102
+ {
6103
+ websiteId: ctx.websiteId,
6104
+ cartToken,
6105
+ contact,
6106
+ fulfillmentFamily: input.fulfillmentFamily,
6107
+ items: input.items.map((item) => ({
6108
+ product_variant_id: item.productVariantId,
6109
+ quantity: item.quantity
6110
+ })),
6111
+ marketingAttribution: input.marketingAttribution ? serializeWebsiteMarketingAttribution(
6112
+ input.marketingAttribution
6113
+ ) : void 0
6114
+ },
6115
+ getHeaders
6116
+ ).catch(explainCheckoutRequestError);
6117
+ return data.websiteStartGuestCheckout;
6118
+ };
6119
+ const existing = cartSessionStorage.get();
6120
+ let result = await requestStartCheckout(existing?.cartToken);
6121
+ if (existing && !result.success && hasRecoverableCartSessionError(result.errors)) {
6122
+ cartSessionStorage.clear();
6123
+ result = await requestStartCheckout();
6124
+ }
6125
+ assertActionSuccess(result, "CHECKOUT_START_ERROR");
6126
+ cartSessionStorage.set({
6127
+ orderId: result.order_id,
6128
+ cartToken: result.cart_token
6129
+ });
6130
+ return mapCart(result.cart, result.cart_token);
6041
6131
  });
6042
- return mapCart(result.cart, result.cart_token);
6043
6132
  },
6044
6133
  async get() {
6045
6134
  const session = cartSessionStorage.get();
@@ -6578,6 +6667,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
6578
6667
  return mapCouponResult(data.websiteRemoveCoupon);
6579
6668
  },
6580
6669
  async submit(input) {
6670
+ const contact = normalizeCheckoutContact(input.contact, "submit");
6581
6671
  const session = requireCartSession2(cartSessionStorage);
6582
6672
  const data = await executeGraphQL(
6583
6673
  graphqlUrl,
@@ -6585,7 +6675,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
6585
6675
  {
6586
6676
  orderId: session.orderId,
6587
6677
  cartToken: session.cartToken,
6588
- contact: input.contact,
6678
+ contact,
6589
6679
  fulfillmentFamily: input.fulfillmentFamily,
6590
6680
  address: input.address,
6591
6681
  shippingQuoteId: input.shippingQuoteId,
@@ -6605,7 +6695,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
6605
6695
  } : void 0
6606
6696
  },
6607
6697
  getHeaders
6608
- );
6698
+ ).catch(explainCheckoutRequestError);
6609
6699
  const result = data.websiteCheckout;
6610
6700
  if (!result.success && result.errors && result.errors.length > 0) {
6611
6701
  throw new CheckoutError(result.errors);
@@ -6646,7 +6736,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
6646
6736
  returnUrl: input.returnUrl
6647
6737
  },
6648
6738
  getHeaders
6649
- );
6739
+ ).catch(explainCheckoutRequestError);
6650
6740
  const result = data.websiteCreatePaymentSession;
6651
6741
  if (result.success && result.payment_session_id && (result.integration_mode !== "redirect" || !result.redirect_url)) {
6652
6742
  throw new WebsiteError(
@@ -6681,7 +6771,7 @@ function createCheckoutModule(graphqlUrl, getHeaders, getContext, cartSessionSto
6681
6771
  gatewayTransactionId: input.gatewayTransactionId
6682
6772
  },
6683
6773
  getHeaders
6684
- );
6774
+ ).catch(explainCheckoutRequestError);
6685
6775
  const result = data.websiteReconcilePaymentSession;
6686
6776
  return {
6687
6777
  success: result.success,
@@ -54,9 +54,16 @@ otros contratos únicamente cuando esas capacidades existan. Leer
54
54
  `STOREFRONT_CONSTRUCTION.md` cuando el website permita ocultarse temporalmente.
55
55
  Si la tienda acepta pagos, leer obligatoriamente `STOREFRONT_PAYMENTS.md` antes
56
56
  de implementar, depurar o auditar checkout, retorno, reintentos o webhooks.
57
- Si existen formularios administrados desde Bacano, leer obligatoriamente
58
- `STOREFRONT_FORMS.md` y
59
- [references/forms.md](references/forms.md) antes de auditarlos o modificarlos.
57
+ En un checkout con autoguardado, comprobar además que no cree pedidos con
58
+ contactos parciales ni solicitudes simultáneas; tras un rechazo, el reintento
59
+ debe conservar el mismo pedido incluso si la sesión de pasarela sigue
60
+ `pending`. Aplicar los casos de [references/storefront.md](references/storefront.md).
61
+ Si existe cualquier formulario visible, leer obligatoriamente
62
+ `STOREFRONT_FORMS.md`. Para formularios propios del storefront, comprobar que
63
+ Zod valida el flujo real, con pruebas de errores por campo y bloqueo del envío
64
+ inválido; su mera presencia en `package.json` no basta. Si el formulario es
65
+ administrado desde Bacano, leer además [references/forms.md](references/forms.md)
66
+ y no duplicar su definición publicada en Zod.
60
67
  Cuando marketing esté activo y existan CTA o formularios, leer
61
68
  [references/interactions.md](references/interactions.md): inventariar los CTA,
62
69
  asignar un propietario por evento y usar los helpers oficiales del SDK. Esta
@@ -40,11 +40,21 @@ función apagada en un bloqueo artificial.
40
40
  ## SEO y compartición
41
41
 
42
42
  - Título, descripción, canonical, robots, Open Graph y Twitter por ruta.
43
+ - Conservar los campos SEO editados de verdad en Bacano, pero no publicar
44
+ `lorem ipsum` u otros borradores visibles como metadata. Usar fallbacks
45
+ específicos para cada página y evitar duplicar la marca en el título.
43
46
  - `robots.txt`, sitemap con URLs canónicas y ausencia de rutas privadas.
47
+ - En el dominio publicado, comprobar con `GET` el código, tipo de contenido y
48
+ política de caché de sitemap y robots. Las rutas estables de descubrimiento
49
+ no deben heredar `immutable` de los assets versionados; contrastar un error
50
+ de Search Console con su prueba en tiempo real antes de culpar al XML. Ver
51
+ `STOREFRONT_PERFORMANCE.md`.
44
52
  - Jerarquía de encabezados, idioma, texto alternativo, enlaces internos y
45
53
  JSON-LD coherente con el contenido visible.
46
54
  - Confirmar 404 real, canonical único, ausencia de slugs/URLs duplicadas y
47
55
  redirección consistente entre `www`, dominio raíz y HTTP/HTTPS.
56
+ - El host del sitemap, canonical, Open Graph y redirección final debe coincidir;
57
+ no basta con que cada URL funcione por separado.
48
58
  - Usar `hreflang` solo cuando existan versiones regionales o de idioma reales.
49
59
  - Favicon, iconos, manifest y vista previa al compartir.
50
60
 
@@ -56,6 +66,9 @@ función apagada en un bloqueo artificial.
56
66
  - Revisar desktop y mobile, DPR 1/2, CLS, LCP, fuentes y JavaScript inicial.
57
67
  Tratar una medición sintética aislada como diagnóstico, no como verdad de
58
68
  campo; repetir y distinguir regresión sostenida de ruido.
69
+ - En carruseles, contar imágenes descargadas, no solo elementos visibles;
70
+ comprobar navegación a fotos lejanas y compatibilidad con los navegadores
71
+ móviles soportados. Ver `STOREFRONT_PERFORMANCE.md`.
59
72
  - Confirmar caché y CORS/CORP de media pública sin proxies locales.
60
73
 
61
74
  ## Accesibilidad y experiencia
@@ -63,6 +76,11 @@ función apagada en un bloqueo artificial.
63
76
  - Navegación por teclado, foco visible, landmarks, labels, contraste y zoom.
64
77
  - Formularios con errores específicos, estados de envío y prevención de doble
65
78
  envío. No usar solo color para comunicar estado.
79
+ - En formularios propios del storefront, verificar Zod en dependencias **y** en
80
+ el código que valida antes del envío; comprobar pruebas para valores válidos,
81
+ inválidos y corrección del error. Si falta, registrar `P1 · Calidad`; si un
82
+ dato inválido llega a checkout o impide comprar, evaluar además el flujo como
83
+ `P0 · Gate`. No exigir un esquema Zod paralelo a `BacanoForm`.
66
84
  - Probar anchos móviles, tablet y escritorio sin scroll interno accidental.
67
85
 
68
86
  ## Legal y privacidad
@@ -25,6 +25,8 @@ ya resuelve la identidad; no ejecutar una segunda captura por campaña.
25
25
  - Reutilizar el único `BacanoProvider` y cliente global del sitio. No crear otro
26
26
  cliente, consultar GraphQL ni llamar endpoints internos.
27
27
  - No duplicar keys, validaciones, opciones o estructura en un esquema local.
28
+ En particular, no imponer un esquema Zod paralelo a la definición publicada;
29
+ Zod corresponde a formularios propios del storefront (ver `STOREFRONT_FORMS.md`).
28
30
  - En Static First, obtener la definición durante build/servidor y entregarla
29
31
  como `initialForm`. La revalidación runtime no debe borrar el HTML inicial.
30
32
  - Renderizar campos directos, secciones visuales y pasos según su semántica. Una
@@ -56,6 +56,18 @@ Aplicar además de los controles comunes.
56
56
  ## Carrito y checkout
57
57
 
58
58
  - Carrito invitado persiste y sincroniza sin duplicar líneas.
59
+ - Los campos propios de checkout se validan con Zod antes de llamar al SDK.
60
+ Inspeccionar el esquema, el punto de envío y pruebas de errores por campo:
61
+ tener Zod instalado o usar solo `type="email"`/`required` no basta. El SDK y
62
+ Bacano mantienen la validación autoritativa; la web no inventa reglas de pago.
63
+ - Si una operación bloquea temporalmente otras mutaciones del carrito, mostrar
64
+ el indicador de carga en el producto accionado, no en todas las tarjetas.
65
+ - Si hay autoguardado, no crear un pedido con datos de contacto parciales.
66
+ Validar los campos requeridos por ese checkout y probar escritura rápida del
67
+ teléfono/correo: una intención de compra mantiene un `orderId` estable.
68
+ - Serializar los inicios de checkout que pueden solaparse (autoguardado, cupón,
69
+ envío y botón final). Confirmar en un trace que el segundo usa el token
70
+ entregado por el primero; no asumir que un debounce evita todas las carreras.
59
71
  - Probar alta, baja, cambios de cantidad, límites de stock, cupón, envío gratis,
60
72
  impuestos, moneda y redondeo.
61
73
  - Probar solo los modos activos entre COUNTER, PICKUP y SHIPPING; propia y
@@ -67,12 +79,20 @@ Aplicar además de los controles comunes.
67
79
 
68
80
  - Leer y aplicar `STOREFRONT_PAYMENTS.md`; no reconstruir en la tienda URLs,
69
81
  credenciales ni payloads privados de Wompi, Mercado Pago o Addi.
82
+ - Si se muestran marcas de pasarela o redes, comprobar que corresponden al
83
+ método efectivamente devuelto por Bacano y no sugieren un método adicional
84
+ ni un medio que el checkout alojado no tenga confirmado.
70
85
  - Ejecutar al menos un happy path real o sandbox por cada método crítico
71
86
  publicado. Probar pago inmediato, transferencia, Wompi y contraentrega solo
72
87
  si están activos; los estados alternos de cada proveedor quedan en su matriz
73
88
  de riesgo.
74
89
  - La página de respuesta no emite `purchase`; Bacano lo hace desde hechos
75
90
  autoritativos. Repetir webhook/conciliación no duplica conversiones.
91
+ - Después de abrir la pasarela, comprobar que el storefront conserva el enlace
92
+ al pedido pero no un carrito local que cree otro pedido al volver. Ante un
93
+ rechazo con sesión todavía `pending`, la respuesta debe ofrecer
94
+ `RETRY_PAYMENT` en el mismo pedido; nunca inferir aprobación o rechazo solo
95
+ del retorno del navegador.
76
96
  - Pedido, pago, entrega y desembolso permanecen conceptos separados.
77
97
  - Probar consulta pública segura y cuenta autenticada sin filtrar pedidos de
78
98
  otra persona, website o canal.