@bacano/sdk 1.2.7 → 1.2.9
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/CATALOG_MANAGEMENT.md +304 -0
- package/README.md +14 -5
- package/STOREFRONT_FORMS.md +38 -0
- package/STOREFRONT_LEADS.md +5 -1
- package/STOREFRONT_PAYMENTS.md +49 -2
- package/catalog-draft-types.d.ts +145 -0
- package/catalog-import-preflight.d.ts +19 -0
- package/catalog-management-types.d.ts +143 -0
- package/catalog-management.cjs +173 -0
- package/catalog-management.d.ts +38 -0
- package/catalog-management.js +150 -0
- package/index.cjs +153 -63
- package/index.js +153 -63
- package/modules/checkout-contact.d.ts +4 -0
- package/package.json +6 -1
- package/react.cjs +153 -63
- package/react.js +153 -63
- package/skills/bacano-web/SKILL.md +10 -3
- package/skills/bacano-web/references/common.md +5 -0
- package/skills/bacano-web/references/forms.md +2 -0
- package/skills/bacano-web/references/storefront.md +15 -0
- package/skills/bacano-web/scripts/cli.mjs +28 -5
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
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
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
|
-
|
|
6123
|
+
CREATE_CART,
|
|
6069
6124
|
{
|
|
6070
6125
|
websiteId: ctx.websiteId,
|
|
6071
|
-
cartToken,
|
|
6072
|
-
contact:
|
|
6073
|
-
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
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
result
|
|
6089
|
-
}
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
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
|
|
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
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
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
|
-
|
|
6070
|
+
CREATE_CART,
|
|
6016
6071
|
{
|
|
6017
6072
|
websiteId: ctx.websiteId,
|
|
6018
|
-
cartToken,
|
|
6019
|
-
contact:
|
|
6020
|
-
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
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
result
|
|
6036
|
-
}
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
@@ -76,6 +76,11 @@ función apagada en un bloqueo artificial.
|
|
|
76
76
|
- Navegación por teclado, foco visible, landmarks, labels, contraste y zoom.
|
|
77
77
|
- Formularios con errores específicos, estados de envío y prevención de doble
|
|
78
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`.
|
|
79
84
|
- Probar anchos móviles, tablet y escritorio sin scroll interno accidental.
|
|
80
85
|
|
|
81
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,8 +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.
|
|
59
63
|
- Si una operación bloquea temporalmente otras mutaciones del carrito, mostrar
|
|
60
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.
|
|
61
71
|
- Probar alta, baja, cambios de cantidad, límites de stock, cupón, envío gratis,
|
|
62
72
|
impuestos, moneda y redondeo.
|
|
63
73
|
- Probar solo los modos activos entre COUNTER, PICKUP y SHIPPING; propia y
|
|
@@ -78,6 +88,11 @@ Aplicar además de los controles comunes.
|
|
|
78
88
|
de riesgo.
|
|
79
89
|
- La página de respuesta no emite `purchase`; Bacano lo hace desde hechos
|
|
80
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.
|
|
81
96
|
- Pedido, pago, entrega y desembolso permanecen conceptos separados.
|
|
82
97
|
- Probar consulta pública segura y cuenta autenticada sin filtrar pedidos de
|
|
83
98
|
otra persona, website o canal.
|