@wallfar/ocd-studio-core-sdk
Version:
Helper SDK for our OneClick Studio modules
209 lines (208 loc) • 8.11 kB
JavaScript
import { validateAddress } from '../../shared/webshop/validateAddress.js';
import Stripe from 'stripe';
export async function validatePromoCode(config, code) {
try {
const { db, promo_codes_collection } = config;
let result = await db.collection(promo_codes_collection).where('code', '==', code).get();
if (result.empty || !result?.docs?.[0]) {
throw new Error();
}
let promo = result.docs[0].data();
if (promo.disabled)
throw new Error();
if (promo.usageLimit !== 0 && promo.usageLimit <= promo.usageCount)
throw new Error('Code is already used');
if (promo.startsAt?.seconds && new Date(promo.startsAt.seconds * 1000) > new Date())
throw new Error();
if (promo.expiresAt?.seconds && new Date(promo.expiresAt.seconds * 1000) < new Date())
throw new Error();
return {
valid: true,
promo: {
id: result.docs[0].id,
code: promo.code,
type: promo.type,
value: promo.value,
minOrderAmount: promo.minOrderAmount,
}
};
}
catch (error) {
return {
valid: false,
error: error.message || 'Code is invalid'
};
}
}
export async function createPaymentLinkStripe(config, stripeConfig, checkoutData) {
if (!stripeConfig.key)
throw new Error('No stripe key provided');
const stripe = new Stripe(stripeConfig.key);
const { cart, shippingOption, activePromoCode, deliveryAddress } = checkoutData;
if (!cart || cart.length === 0)
throw new Error('No products in cart');
if (!shippingOption)
throw new Error('No shipping option selected');
if (!deliveryAddress)
throw new Error('No delivery address');
let validatedCart = [];
let subtotal = 0;
let discountObject = null;
// 1. Get correct cart data
const products = await Promise.all(cart.map(async (item) => {
const product = await config.db.collection(config.products_collection).doc(item.productId).get();
return { id: product.id, ...product.data() };
}));
cart.forEach((cart_item) => {
let product = products.find(i => i.id == cart_item.productId);
let stock = product.stock, price = product.price;
if (Array.isArray(product.options) && product.options.length > 0) {
let variant = getVariant(product, cart_item.variant);
stock = variant.stock;
price = variant.price;
}
let new_item = {
id: cart_item.productId,
slug: product.slug,
title: product.title,
featuredMedia: product.featuredMedia,
variant: cart_item.variant,
quantity: stock < cart_item.quantity ? stock : cart_item.quantity,
price
};
validatedCart.push(new_item);
subtotal += (new_item.quantity * new_item.price);
});
// 2. Validate promo code
if (activePromoCode) {
let { valid, promo, error } = await validatePromoCode(config, activePromoCode);
if (!valid)
throw new Error(error);
discountObject = promo;
}
// 3. Validate shipping option
const { valid: isValidShippingOption, price: shippingFee } = await validateShippingOption(config, shippingOption, deliveryAddress, subtotal);
if (!isValidShippingOption)
throw new Error('Invalid shipping option');
// 4. Validate delivery address
const isValidAddress = validateAddress(deliveryAddress);
if (isValidAddress.length > 0)
throw new Error(isValidAddress.join(', '));
// 5. Calculate total
const discount = discountObject ? calculateDiscount(discountObject, subtotal) : 0;
let total = subtotal - discount + (shippingFee || 0);
if (total <= 0)
total = 0;
console.log("stripe = ", {
price_data: {
currency: 'eur',
unit_amount: (total * 100).toFixed(0),
product_data: {
name: "Order",
images: []
}
},
quantity: 1
});
// 6. Create payment link
const session = await stripe.checkout.sessions.create({
customer_email: "maxim@wallfar.com",
payment_method_types: ['card', 'bancontact', 'ideal', 'sofort'],
line_items: [{
price_data: {
currency: 'eur',
unit_amount: parseInt((total * 100).toFixed(0)),
product_data: {
name: "Order",
images: []
}
},
quantity: 1
}],
mode: 'payment',
success_url: 'https://soakhandmade.be/checkout/success',
cancel_url: 'https://soakhandmade.be/checkout/cancel',
metadata: {
fbase_order_id: "fbase_order_id"
},
payment_intent_data: {
metadata: {
fbase_order_id: "fbase_order_id",
}
}
});
if (!session.url)
throw new Error('Could not create payment link');
return session.url;
}
async function validateShippingOption(config, optionId, deliveryAddress, subtotal) {
const shippingOptions = await getAllShippingOptions(config);
const shippingOption = shippingOptions?.find(opt => opt.id === optionId);
if (!shippingOption)
return { valid: false };
if (shippingOption.status !== 'published')
return { valid: false };
const validCountry = !shippingOption.countries || shippingOption.countries.length === 0 || shippingOption.countries.includes(deliveryAddress.country);
if (!validCountry)
return { valid: false };
let validCondition = shippingOption.condition === 'always';
if (!validCondition) {
if (shippingOption.condition === 'price_based') {
const MIN = shippingOption.condition_min || 0;
const MAX = shippingOption.condition_max || Infinity;
validCondition = subtotal >= MIN && subtotal <= MAX;
}
else if (shippingOption.condition === 'weight_based') {
// TODO
}
}
return {
valid: validCountry && validCondition,
price: shippingOption.price || 0
};
}
async function getAllShippingOptions(config) {
const { shipping_options_collection } = config;
if (!shipping_options_collection)
return null;
if (shipping_options_collection.includes(':')) {
const collectionParts = shipping_options_collection.split(':');
const pathParts = collectionParts[0].split('/');
const path = pathParts.slice(0, pathParts.length - 1);
const docId = pathParts[pathParts.length - 1];
const docSnap = await config.db.collection(path.join('/')).doc(docId).get();
const options = docSnap.data()?.[collectionParts[1]];
return options?.filter(opt => opt.status === 'published') || null;
}
else {
const snapshot = await config.db.collection(shipping_options_collection).get();
return !snapshot.empty ? snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })).filter((opt) => opt.status === 'published') : null;
}
}
function getVariant(product, variant) {
if (!product.variants || product.variants?.length === 0) {
return {
id: product.id,
options: [],
combinedOptions: '',
stock: product.stock,
price: product.price,
featuredMedia: product.featuredMedia,
disabled: false,
title: product.title,
slug: product.slug
};
}
return product.variants?.find((v) => v.options.every((o) => o.value === variant[o.option]));
}
function calculateDiscount(promo, subtotal) {
if (!promo)
return 0;
if (promo.type === 'percentage') {
return (subtotal * ((promo.value || 0) / 100)) || 0;
}
if (promo.type === 'fixed') {
return promo.value || 0;
}
return 0;
}