From 137208e18a8d1399f298bb1e57daa6a0137cc1b3 Mon Sep 17 00:00:00 2001 From: Wisley Alves Date: Fri, 22 Sep 2023 17:50:10 -0300 Subject: [PATCH] fix(galaxPay): Update App --- .../galaxpay/src/functions-lib/all-parses.ts | 6 - .../src/functions-lib/ecom/request-api.ts | 28 ++ .../src/functions-lib/firestore/utils.ts | 80 ++++ .../galaxpay/auth/create-access.ts | 2 +- .../functions-lib/galaxpay/handle-plans.ts | 37 +- .../galaxpay/update-subscription.ts | 350 +++++++++++++++--- .../src/functions-lib/galaxpay/webhook.ts | 333 ++++++++++------- .../src/galaxpay-create-transaction.ts | 18 +- .../galaxpay/src/galaxpay-list-payments.ts | 27 +- packages/apps/galaxpay/types/config-app.d.ts | 9 +- 10 files changed, 664 insertions(+), 226 deletions(-) create mode 100644 packages/apps/galaxpay/src/functions-lib/ecom/request-api.ts create mode 100644 packages/apps/galaxpay/src/functions-lib/firestore/utils.ts diff --git a/packages/apps/galaxpay/src/functions-lib/all-parses.ts b/packages/apps/galaxpay/src/functions-lib/all-parses.ts index 48896ff57..07378152a 100644 --- a/packages/apps/galaxpay/src/functions-lib/all-parses.ts +++ b/packages/apps/galaxpay/src/functions-lib/all-parses.ts @@ -3,11 +3,6 @@ // type Gateway = ListPaymentsResponse['payment_gateways'][number] -const gerateId = (id: string | number) => { - const length = 24 - id.toString().length + 1; - return Array(length).join('0') + id; -}; - const parsePeriodicityToEcom = (periodicity?: string) => { switch (periodicity) { case 'weekly': @@ -84,7 +79,6 @@ const parsePeriodicityToGalaxPay = (periodicity?: string) => { }; export { - gerateId, parsePeriodicityToEcom, parsePeriodicityToGalaxPay, parseStatus, diff --git a/packages/apps/galaxpay/src/functions-lib/ecom/request-api.ts b/packages/apps/galaxpay/src/functions-lib/ecom/request-api.ts new file mode 100644 index 000000000..6240722e3 --- /dev/null +++ b/packages/apps/galaxpay/src/functions-lib/ecom/request-api.ts @@ -0,0 +1,28 @@ +import api from '@cloudcommerce/api'; + +const findOrderById = async (orderId: string) => { + const { data } = await api.get(`orders/${orderId}`); + return data; +}; + +const getProductsById = async (productId:string) => { + const { data } = await api.get(`products/${productId}`); + return data; +}; + +const getOrderWithQueryString = async (queryString:string) => { + const { data } = await api.get(`orders?${queryString}`); + return data; +}; + +const getOrderIntermediatorTId = (tId: string) => { + const queryString = `?transactions.intermediator.transaction_id=${tId}`; + return getOrderWithQueryString(queryString); +}; + +export { + findOrderById, + getOrderIntermediatorTId, + getOrderWithQueryString, + getProductsById, +}; diff --git a/packages/apps/galaxpay/src/functions-lib/firestore/utils.ts b/packages/apps/galaxpay/src/functions-lib/firestore/utils.ts new file mode 100644 index 000000000..0aa2d2746 --- /dev/null +++ b/packages/apps/galaxpay/src/functions-lib/firestore/utils.ts @@ -0,0 +1,80 @@ +import type { Orders } from '@cloudcommerce/types'; +import logger from 'firebase-functions/logger'; + +const getDocSubscription = ( + orderId, + collectionSubscription, +) => new Promise((resolve, reject) => { + const subscription = collectionSubscription.doc(orderId); + + subscription.get() + .then((documentSnapshot) => { + if (documentSnapshot.exists) { + const data = documentSnapshot.data(); + if (data.storeId) { + resolve(data); + } else { + reject(new Error('StoreId property not found in document')); + } + } else { + reject(new Error('Document does not exist Firestore')); + } + }).catch((err) => { + reject(err); + }); +}); + +const updateDocSubscription = async ( + collectionSubscription, + body, + subscriptionId, +) => { + const updatedAt = new Date().toISOString(); + body.updatedAt = updatedAt; + + await collectionSubscription.doc(subscriptionId) + .set(body, { merge: true }) + .catch(logger.error); +}; + +const createItemsAndAmount = ( + amount: Orders['amount'], + items: Orders['items'], +) => { + const itemsAndAmount = { + amount, + items: items?.reduce(( + accumulator: { + sku?: string, + final_price?: number, + price:number, + quantity: number, + product_id: string & {length: 24} + }[], + itemOrder, + ) => { + const item = { + sku: itemOrder.sku, + final_price: itemOrder.final_price, + price: itemOrder.price, + quantity: itemOrder.quantity, + product_id: itemOrder.product_id, + }; + + if (itemOrder.variation_id) { + Object.assign(item, { variation_id: itemOrder.variation_id }); + } + + accumulator.push(item); + return accumulator; + }, []), + }; + + return itemsAndAmount; +}; + +export { + getDocSubscription, + updateDocSubscription, + createItemsAndAmount, +}; diff --git a/packages/apps/galaxpay/src/functions-lib/galaxpay/auth/create-access.ts b/packages/apps/galaxpay/src/functions-lib/galaxpay/auth/create-access.ts index 4c8cdcd17..66fe90f94 100644 --- a/packages/apps/galaxpay/src/functions-lib/galaxpay/auth/create-access.ts +++ b/packages/apps/galaxpay/src/functions-lib/galaxpay/auth/create-access.ts @@ -64,7 +64,7 @@ export default class GalaxPay { .then((documentSnapshot) => { if (documentSnapshot.exists && Date.now() - documentSnapshot.updateTime.toDate().getTime() - <= 9 * 60 * 1000 // access token expires in 10 minutes + <= 5 * 60 * 1000 // access token expires in 5 minutes ) { authenticate(documentSnapshot.get('accessToken')); } else { diff --git a/packages/apps/galaxpay/src/functions-lib/galaxpay/handle-plans.ts b/packages/apps/galaxpay/src/functions-lib/galaxpay/handle-plans.ts index 5790dbcbf..7dfffeace 100644 --- a/packages/apps/galaxpay/src/functions-lib/galaxpay/handle-plans.ts +++ b/packages/apps/galaxpay/src/functions-lib/galaxpay/handle-plans.ts @@ -1,7 +1,8 @@ import type { GalaxpayApp } from '../../../types/config-app'; import type { ListPaymentsParams } from '@cloudcommerce/types'; -// type Gateway = ListPaymentsResponse['payment_gateways'][number] +type Plan = Exclude[number] +type Amount = Exclude const handleGateway = (appData: GalaxpayApp) => { const plans: Exclude = []; @@ -40,15 +41,27 @@ const findPlanToCreateTransction = (label: string | undefined, appData: Galaxpay }; const discountPlan = ( - planDiscount: Exclude[number]['discount'], - amount: Exclude, + planName: string, + plan: Plan, + amount: Omit & { total?: number }, ) => { + let planDiscount: Plan['discount']; + if (plan.discount_first_installment && !plan.discount_first_installment?.disable) { + planDiscount = plan.discount_first_installment; + } else { + planDiscount = plan.discount; + } + if (planDiscount && planDiscount.value > 0) { // default discount option + const type = planDiscount.type; + + const applyAt: string = planDiscount.apply_at === 'frete' ? 'freight' : planDiscount.apply_at; + const discountOption = { - value: planDiscount.value, - apply_at: (planDiscount.apply_at === 'frete' ? 'freight' : planDiscount) as 'total' | 'subtotal' | 'freight', - type: planDiscount.percentage ? 'percentage' : 'fixed' as 'percentage' | 'fixed' | undefined, + label: planName, + value: planDiscount.value as number | undefined, + type, }; if (amount.total) { @@ -58,15 +71,16 @@ const discountPlan = ( } else { delete planDiscount.min_amount; - const maxDiscount = amount[discountOption.apply_at || 'subtotal']; + const maxDiscount = amount[applyAt || 'subtotal']; let discountValue: number | undefined; if (maxDiscount && discountOption.type === 'percentage') { discountValue = (maxDiscount * planDiscount.value) / 100; } else { discountValue = planDiscount.value; - if (maxDiscount && discountValue > maxDiscount) { - discountValue = maxDiscount; + if (maxDiscount) { + discountValue = (discountValue && discountValue > maxDiscount) + ? maxDiscount : discountValue; } } @@ -79,7 +93,10 @@ const discountPlan = ( } } } - return discountOption; + const discount = planDiscount as Omit + & { apply_at: 'total' | 'subtotal' | 'freight' }; + + return { amount, discountOption, discount }; } return null; }; diff --git a/packages/apps/galaxpay/src/functions-lib/galaxpay/update-subscription.ts b/packages/apps/galaxpay/src/functions-lib/galaxpay/update-subscription.ts index f82f34d86..d9e852e3a 100644 --- a/packages/apps/galaxpay/src/functions-lib/galaxpay/update-subscription.ts +++ b/packages/apps/galaxpay/src/functions-lib/galaxpay/update-subscription.ts @@ -1,92 +1,326 @@ -import type { Orders, Applications } from '@cloudcommerce/types'; +import type { Orders } from '@cloudcommerce/types'; import logger from 'firebase-functions/logger'; +import axios from 'axios'; +import config from '@cloudcommerce/firebase/lib/config'; +import { + getProductsById, +} from '../ecom/request-api'; import GalaxpayAxios from './auth/create-access'; -const checkAmountItemsOrder = ( +type Dimensions = { + [k: string]: { + value: number; + unit?: 'mm' | 'cm' | 'm' | 'ft' | 'in' | 'yd' | 'mi'; + } +} + +type Weight = { + value: number; + unit?: 'mg' | 'g' | 'kg' | 'lb' | 'oz'; +} + +type Item = Exclude[number] + & { dimensions?: Dimensions } & { weight?: Weight } + +const getNewFreight = async ( + itemsOrder: Item[], + to, + subtotal:number, + shippingLineOriginal, +) => { + const { settingsContent } = config.get(); + const urlApiModule = `https://${settingsContent.domain}/api/modules`; + + if (!shippingLineOriginal.app) return null; + const items: Item[] = []; + let i = 0; + + while (i < itemsOrder.length) { + const item = itemsOrder[i]; + if (!item.dimensions) { + // add dimensions for shipping calculation + // eslint-disable-next-line no-await-in-loop + const product = await getProductsById(item.product_id); + let dimensions = product?.dimensions; + let weight = product?.weight; + + if (item.variation_id) { + const variation = product?.variations?.find((itemFind) => itemFind.sku === item.sku); + if (variation?.dimensions) { + dimensions = variation.dimensions; + } + if (variation?.weight) { + weight = variation.weight; + } + } + items.push({ ...item, dimensions, weight }); + } else { + items.push({ ...item }); + } + i += 1; + } + + const body = { + items, + subtotal, + to, + }; + + try { + const headers = { + accept: 'application/json', + }; + + const { data: { result } } = await axios.post( + urlApiModule, + body, + { headers }, + ); + + if (!result.length) return null; + + const sameApp = result.find((appFind) => appFind._id === shippingLineOriginal.app._id); + + if (sameApp) { + const service = sameApp.response?.shipping_services?.find( + (serviceFind) => serviceFind.service_code === shippingLineOriginal.app.service_code, + ); + + return service || sameApp.response?.shipping_services[0]; + } + + let minPrice = result[0]?.response?.shipping_services[0]?.shipping_line?.total_price; + const indexPosition = { app: 0, service: 0 }; + + for (let index = 0; index < result.length; index++) { + const app = result[index]; + + for (let j = 0; j < app.response?.shipping_services.length; j++) { + const service = app.response?.shipping_services[j]; + + if (service.service_code === shippingLineOriginal.app.service_code) { + return service; + } + + if (minPrice > service?.shipping_line?.total_price) { + minPrice = service?.shipping_line?.total_price; + indexPosition.app = index; + indexPosition.service = j; + } + } + } + + return result[indexPosition.app]?.response?.shipping_services[indexPosition.service]; + } catch (err) { + logger.error(err); + return null; + } +}; + +const checkItemsAndRecalculeteOrder = async ( amount: Orders['amount'], items: Exclude, - plan: { [x: string]: any }, + plan, + newItem, + shippingLine?: Exclude[number], ) => { let subtotal = 0; let item: Exclude[number]; - for (let i = 0; i < items.length; i++) { + let i = 0; + while (i < items.length) { item = items[i]; - if (item.flags && (item.flags.includes('freebie') || item.flags.includes('discount-set-free'))) { + if (newItem && item.sku === newItem.sku) { + if (newItem.quantity === 0) { + items.splice(i, 1); + } else { + if (item.final_price) { + item.final_price = newItem.price; + } + item.price = newItem.price; + item.quantity = newItem.quantity; + subtotal += item.quantity * (item.final_price || item.price); + i += 1; + } + } else if (item.flags && (item.flags.includes('freebie') || item.flags.includes('discount-set-free'))) { items.splice(i, 1); } else { subtotal += item.quantity * (item.final_price || item.price); + i += 1; + } + } + + if (subtotal > 0) { + if (shippingLine) { + const service = await getNewFreight(items, shippingLine?.to, subtotal, shippingLine); + + if (service && service?.shipping_line?.total_price) { + shippingLine = { ...shippingLine, ...service.shipping_line }; + if (shippingLine) { + delete shippingLine._id; + } + amount.freight = service.shipping_line.total_price; + } } + + amount.subtotal = subtotal; + amount.total = amount.subtotal + (amount.tax || 0) + + (amount.freight || 0) + (amount.extra || 0); + + let planDiscount; + if (plan && plan.discount) { + if (plan.discount.percentage || plan.discount.type === 'percentage') { + planDiscount = amount[plan.discount.apply_at]; + planDiscount *= ((plan.discount.value) / 100); + } else { + planDiscount = plan.discount.value; + } + } + + amount.discount = planDiscount || amount.discount || 0; + + amount.total -= amount.discount || 0; + return amount.total > 0 ? Math.floor(parseFloat(amount.total.toFixed(2)) * 1000) / 10 : 0; } - amount.subtotal = subtotal; - amount.total = amount.subtotal + (amount.tax || 0) + (amount.freight || 0) + (amount.extra || 0); - let planDiscount; - if (plan && plan.discount) { - if (plan.discount.percentage) { - planDiscount = amount[plan.discount.apply_at]; - planDiscount *= ((plan.discount.value) / 100); + return 0; +}; + +const getSubscriptionsByListMyIds = async ( + galaxpayAxios, + listOrders, +) => { + // Consultation on galaxpay has a limit of 100 per request + const promises: any[] = []; // TODO: + try { + let myIds = ''; + await galaxpayAxios.preparing; + // Handle when there are more than 100 orders + for (let i = 0; i < listOrders.length; i++) { + if ((i + 1) % 100 !== 0 && (i + 1) !== listOrders.length) { + myIds += `${listOrders[i]},`; + } else if ((i + 1) !== listOrders.length) { + promises.push( + galaxpayAxios.axios.get(`/subscriptions?myIds=${myIds}&startAt=0&&limit=100&&status=active`), + ); + myIds = ''; + } else { + myIds += `${listOrders[i]},`; + promises.push( + galaxpayAxios.axios.get(`/subscriptions?myIds=${myIds}&startAt=0&&limit=100&&status=active`), + ); + } } + + const galaxPaySubscriptions = (await Promise.all(promises)) + ?.reduce((subscriptions, { data }) => { + if (data?.Subscriptions) { + subscriptions.push(...data.Subscriptions); + } + return subscriptions; + }, []); + + return galaxPaySubscriptions; + } catch (err) { + logger.error(err); + return null; } - // if the plan doesn't exist, because it's subscription before the update - if (plan) { - amount.discount = (plan.discount && !plan.discount.percentage - ? plan.discount.value : planDiscount) || 0; +}; + +const updateValueSubscriptionGalaxpay = async ( + galaxpayAxios, + subscriptionId: string, + value: number, + oldValue: number, +) => { + if (!oldValue) { + const { data } = await galaxpayAxios.axios.get(`subscriptions?myIds=${subscriptionId}&startAt=0&limit=1`); + oldValue = data.Subscriptions[0] && data.Subscriptions[0].value; } - if (amount.discount) { - amount.total -= amount.discount; + + if (oldValue !== value) { + const { data } = await galaxpayAxios.axios.put(`subscriptions/${subscriptionId}/myId`, { value }); + if (data.type) { + logger.log(`> Update [GP] => ${subscriptionId}: ${oldValue} to ${value}`); + return value; + } } - const total: any = amount.total - (amount.discount || 0); // BUG :( - return Math.floor(total.toFixed(2) * 100); + return null; }; -const updateValueSubscription = ( - appData: Applications, +const checkAndUpdateSubscriptionGalaxpay = async ( subscriptionId: string, amount: Orders['amount'], items: Exclude, - plan: { [x: string]: any }, - // GalaxPaySubscription: { [x: string]: any }, + plan, + oldValue: number, + shippingLine?: Exclude[number], ) => { - const value = checkAmountItemsOrder({ ...amount }, [...items], { ...plan }); + let copyShippingLine: Exclude[number] | undefined; - if (!process.env.GALAXPAY_ID) { - const galaxpayId = appData.hidden_data?.galaxpay_id; - if (typeof galaxpayId === 'string' && galaxpayId) { - process.env.GALAXPAY_ID = galaxpayId; - } else { - logger.warn('Missing GalaxPay ID'); - } + if (shippingLine) { + copyShippingLine = { ...shippingLine }; } + const value = await checkItemsAndRecalculeteOrder( + { ...amount }, + [...items], + { ...plan }, + null, + copyShippingLine, + ); - if (!process.env.GALAXPAY_HASH) { - const galaxpayHash = appData.hidden_data?.galaxpay_hash; - if (typeof galaxpayHash === 'string' && galaxpayHash) { - process.env.GALAXPAY_HASH = galaxpayHash; - } else { - logger.warn('Missing GalaxPay ID'); - } - } + const galaxpayAxios = new GalaxpayAxios({ + galaxpayId: process.env.GALAXPAY_ID, + galaxpayHash: process.env.GALAXPAY_HASH, + }); + + await galaxpayAxios.preparing; + return updateValueSubscriptionGalaxpay(galaxpayAxios, subscriptionId, value, oldValue); +}; + +const compareDocItemsWithOrder = ( + docItemsAndAmount, + originalItems:Orders['items'], + originalAmount: Orders['amount'], + galapayTransactionValue, +) => { + const finalAmount = Math.floor(parseFloat((originalAmount.total).toFixed(2)) * 1000) / 1000; - return new Promise((resolve, reject) => { - const galaxpayAxios = new GalaxpayAxios({ - galaxpayId: process.env.GALAXPAY_ID, - galaxpayHash: process.env.GALAXPAY_HASH, - }); - - galaxpayAxios.preparing - .then(async () => { - if (galaxpayAxios.axios) { - const { data } = await galaxpayAxios.axios.put(`subscriptions/${subscriptionId}/myId`, { value }); - if (data.type) { - resolve(true); + logger.log(`Compare values: ${ + JSON.stringify(originalAmount) + } => total: ${finalAmount} GP: ${galapayTransactionValue}`); + + if (galapayTransactionValue !== finalAmount) { + // need update itens and recalculate order + let i = 0; + if (originalItems?.length) { + while (i < originalItems.length) { + const itemOrder = originalItems[i]; + const itemDoc = docItemsAndAmount?.items?.find( + (itemFind: { sku: any; }) => itemFind.sku === itemOrder.sku, + ); + if (itemDoc) { + if (itemOrder.price !== itemDoc.price) { + itemOrder.price = itemDoc.price; + if (itemOrder.final_price !== itemDoc.final_price) { + itemOrder.final_price = itemDoc.final_price; + } } + + if (itemOrder.quantity !== itemDoc.quantity) { + itemOrder.quantity = itemDoc.quantity; + } + i += 1; + } else { + originalItems.splice(i, 1); } - }).catch((err) => { - reject(err); - }); - }); + } + } + } }; export { - updateValueSubscription, - checkAmountItemsOrder, + checkAndUpdateSubscriptionGalaxpay, + checkItemsAndRecalculeteOrder, + updateValueSubscriptionGalaxpay, + getSubscriptionsByListMyIds, + compareDocItemsWithOrder, }; diff --git a/packages/apps/galaxpay/src/functions-lib/galaxpay/webhook.ts b/packages/apps/galaxpay/src/functions-lib/galaxpay/webhook.ts index acd006e17..8645fa15d 100644 --- a/packages/apps/galaxpay/src/functions-lib/galaxpay/webhook.ts +++ b/packages/apps/galaxpay/src/functions-lib/galaxpay/webhook.ts @@ -1,7 +1,6 @@ import type { Orders, Applications, - ResourceListResult, ResourceId, } from '@cloudcommerce/types'; import type { Request, Response } from 'firebase-functions'; @@ -9,13 +8,23 @@ import api from '@cloudcommerce/api'; import { getFirestore } from 'firebase-admin/firestore'; import logger from 'firebase-functions/logger'; import config from '@cloudcommerce/firebase/lib/config'; -import { parseStatus, parsePeriodicityToEcom, gerateId } from '../all-parses'; +import { parseStatus, parsePeriodicityToEcom } from '../all-parses'; +import { + findOrderById, + getOrderIntermediatorTId, +} from '../ecom/request-api'; +import { createItemsAndAmount } from '../firestore/utils'; import GalaxpayAxios from './auth/create-access'; -import { updateValueSubscription, checkAmountItemsOrder } from './update-subscription'; +import { + checkAndUpdateSubscriptionGalaxpay, + checkItemsAndRecalculeteOrder, + compareDocItemsWithOrder, +} from './update-subscription'; type FinancialStatusCurrent = Exclude['current'] const collectionSubscription = getFirestore().collection('galaxpaySubscriptions'); +const collectionTransactions = getFirestore().collection('galaxpayTransactions'); const getApp = async (): Promise => { return new Promise((resolve, reject) => { @@ -58,91 +67,115 @@ const checkPayDay = (strDate: string) => { return (now >= payDay); }; -const findOrderByTransactionId = (transactionId: string): Promise> => { - return new Promise((resolve, reject) => { - api.get(`orders?transactions._id=${transactionId}`) - .then(({ data: response }) => { - const resp = response as unknown as ResourceListResult<'orders'>; // TODO: - resolve(resp); - }) - .catch((err) => { - reject(err); - }); - }); -}; - -const findOrderById = async (orderId: ResourceId): Promise => new Promise((resolve) => { - api.get(`orders/${orderId}`) - .then(({ data: order }) => { - resolve(order); - }); -}); - const createTransaction = async ( res: Response, subscriptionLabel: string, - plan: { [x: string]: any }, + docSubscription, orderNumber: string, galaxpayFristTransactionId: string, - GalaxPayTransaction: { [x: string]: any }, - GalaxPaySubscription: { [x: string]: any }, - GalaxPayTransactionValue: number, + galaxPayTransaction, + galaxPaySubscription, + galaxPayTransactionValue: number, originalOrderId: ResourceId, ) => { - if (galaxpayFristTransactionId !== GalaxPayTransaction.galaxPayId) { + if (galaxpayFristTransactionId !== galaxPayTransaction.galaxPayId) { // let body; const originalOrder = (await api.get(`orders/${originalOrderId}`)).data; // logger.log('> Create new Order ') if (originalOrder.transactions && originalOrder.items) { - const { installment } = GalaxPayTransaction; + const { installment } = galaxPayTransaction; const { - buyers, items, domain, amount, + buyers, + items, + domain, + amount, } = originalOrder; + + const { plan } = docSubscription; + let { itemsAndAmount } = docSubscription; + + try { + const transactionDoc = (await collectionTransactions + .doc(`${galaxPayTransaction.galaxPayId}`).get())?.data(); + + itemsAndAmount = transactionDoc?.itemsAndAmount; + // logger.log('>> items to transaction'); + } catch (err) { + logger.warn(err); + } + const channelType = originalOrder.channel_type; const shippingLines = originalOrder.shipping_lines; const shippingMethodLabel = originalOrder.shipping_method_label; const paymentMethodLabel = originalOrder.payment_method_label; const originalTransaction = originalOrder.transactions[0]; const quantity = installment; - const periodicity = parsePeriodicityToEcom(GalaxPaySubscription.periodicity); - const dateUpdate = GalaxPayTransaction.datetimeLastSentToOperator - ? new Date(GalaxPayTransaction.datetimeLastSentToOperator).toISOString() + const periodicity = parsePeriodicityToEcom(galaxPaySubscription.periodicity); + const dateUpdate = galaxPayTransaction.datetimeLastSentToOperator + ? new Date(galaxPayTransaction.datetimeLastSentToOperator).toISOString() : new Date().toISOString(); - // remove items free in new orders subscription - checkAmountItemsOrder(amount, items, plan); + if (itemsAndAmount && itemsAndAmount.items?.length) { + compareDocItemsWithOrder(itemsAndAmount, items, amount, galaxPayTransactionValue); + } + // recalculate order + const shippingLine = shippingLines && shippingLines[0]; + await checkItemsAndRecalculeteOrder(amount, items, plan, null, shippingLine); + + if (shippingLines?.length && shippingLine) { + shippingLines[0] = shippingLine; + } + if (amount.balance) { delete amount.balance; } - const transactionId = String(gerateId(GalaxPayTransaction.galaxPayId)); + items.forEach((item) => { + if (item.stock_status && item.stock_status !== 'unmanaged') { + item.stock_status = 'pending'; + } + }); + + const tId = galaxPayTransaction.tid; const transactions: Orders['transactions'] = [ { amount: originalTransaction.amount, status: { updated_at: dateUpdate, - current: parseStatus(GalaxPayTransaction.status), + current: parseStatus(galaxPayTransaction.status), }, intermediator: { - transaction_id: GalaxPayTransaction.tid || '', - transaction_code: GalaxPayTransaction.authorizationCode || '', + transaction_id: tId, + transaction_code: galaxPayTransaction.authorizationCode || '', + transaction_reference: galaxPayTransaction.tid || '', }, payment_method: originalTransaction.payment_method, app: originalTransaction.app, - _id: transactionId as ResourceId, notes: `Parcela #${quantity} referente à ${subscriptionLabel} ${periodicity}`, custom_fields: originalTransaction.custom_fields, }, ]; - transactions[0].payment_link = GalaxPaySubscription.paymentLink; + transactions[0].payment_link = galaxPaySubscription.paymentLink; const financialStatus = { updated_at: dateUpdate, - current: parseStatus(GalaxPayTransaction.status) as FinancialStatusCurrent, + current: parseStatus(galaxPayTransaction.status) as FinancialStatusCurrent, }; + + const planPergentage = plan?.discount + ? plan.discount.type === 'percentage' || plan.discount.percentage + : null; + + let notes = `Parcela #${quantity} desconto de ${planPergentage ? '' : 'R$'}`; + if (planPergentage) { + notes += ` ${plan?.discount?.value || ''} ${planPergentage ? '%' : ''}`; + notes += ` sobre ${plan?.discount?.apply_at || ''}`; + notes += ` referente à ${subscriptionLabel} ${periodicity}`; + } + const body = { opened_at: new Date().toISOString(), items, @@ -159,16 +192,28 @@ const createTransaction = async ( _id: originalOrderId as string & { length: 24 }, number: parseInt(orderNumber, 10), }, - notes: `Pedido #${quantity} referente à ${subscriptionLabel} ${periodicity}`, - staff_notes: `Valor cobrado no GalaxPay R$${GalaxPayTransactionValue}`, + notes, + staff_notes: `Valor cobrado no GalaxPay R$${galaxPayTransactionValue}`, }; - const { result } = await findOrderByTransactionId(transactionId); + const finalAmount = Math.floor(parseFloat((amount.total).toFixed(2)) * 1000) / 1000; + + if (galaxPayTransactionValue !== finalAmount) { + logger.warn(`#${galaxPayTransaction.galaxPayId}] amount: ${ + JSON.stringify(amount) + }, Galaxpay value: ${galaxPayTransactionValue}, items: ${JSON.stringify(items)},`); + } + + const { result } = await getOrderIntermediatorTId(tId); if (!result.length) { await api.post('orders', body); // logger.log('> Created new order API') - return res.sendStatus(200); + await collectionTransactions.doc(`${galaxPayTransaction.galaxPayId}`) + .delete() + .catch(logger.error); + + return res.sendStatus(201); } // Order Exists return res.sendStatus(200); @@ -184,17 +229,17 @@ const handleWehook = async (req: Request, res: Response) => { // POST transaction.updateStatus update Transation status // POST subscription.addTransaction add transation in subscription - const galaxpayHook = req.body; - const type = galaxpayHook.event; - const GalaxPaySubscription = galaxpayHook.Subscription; - const GalaxPaySubscriptionQuantity = GalaxPaySubscription.quantity; - const originalOrderId = GalaxPaySubscription.myId; - const GalaxPayTransaction = galaxpayHook.Transaction; - const GalaxPayTransactionValue = GalaxPayTransaction.value / 100; + const whGalaxPay = req.body; + const type = whGalaxPay.event; + const whGalaxPaySubscription = whGalaxPay.Subscription; + const whGalaxPaySubscriptionQuantity = whGalaxPaySubscription.quantity; + const originalOrderId = whGalaxPaySubscription.myId; + const whGalaxPayTransaction = whGalaxPay.Transaction; + const whGalaxPayTransactionValue = whGalaxPayTransaction.value / 100; logger.log( - `> (App GalaxPay) WebHook ${type}, Body ${JSON.stringify(galaxpayHook)}, quantity: - ${GalaxPaySubscriptionQuantity}, status: ${GalaxPayTransaction.status} <`, + `> (App GalaxPay) WebHook ${type}, Body ${JSON.stringify(whGalaxPay)}, quantity: + ${whGalaxPaySubscriptionQuantity}, status: ${whGalaxPayTransaction.status} <`, ); try { @@ -208,7 +253,7 @@ const handleWehook = async (req: Request, res: Response) => { plan, orderNumber, subscriptionLabel, - } = docSubscription.data(); + } = docSubscription; let galaxPayTransactionStatus: string | undefined; let galaxpaySubscriptionStatus: string | undefined; @@ -219,7 +264,7 @@ const handleWehook = async (req: Request, res: Response) => { // check subscription and transaction status before in galaxpay if (!process.env.GALAXPAY_ID) { const galaxpayId = app.hidden_data?.galaxpay_id; - if (typeof galaxpayId === 'string' && galaxpayId) { + if (galaxpayId && typeof galaxpayId === 'string') { process.env.GALAXPAY_ID = galaxpayId; } else { logger.warn('Missing GalaxPay ID'); @@ -228,7 +273,7 @@ const handleWehook = async (req: Request, res: Response) => { if (!process.env.GALAXPAY_HASH) { const galaxpayHash = app.hidden_data?.galaxpay_hash; - if (typeof galaxpayHash === 'string' && galaxpayHash) { + if (galaxpayHash && typeof galaxpayHash === 'string') { process.env.GALAXPAY_HASH = galaxpayHash; } else { logger.warn('Missing GalaxPay Hash'); @@ -243,7 +288,7 @@ const handleWehook = async (req: Request, res: Response) => { if (galaxpayAxios.axios) { let { data } = await galaxpayAxios.axios - .get(`/transactions?galaxPayIds=${GalaxPayTransaction.galaxPayId}&startAt=0&limit=1`); + .get(`/transactions?galaxPayIds=${whGalaxPayTransaction.galaxPayId}&startAt=0&limit=1`); galaxPayTransactionStatus = data.Transactions[0]?.status; const dateTimeTransaction = data.Transactions[0]?.createdAt; @@ -260,32 +305,40 @@ const handleWehook = async (req: Request, res: Response) => { logger.warn(`galaxpay webhook Error: get Transaction/Subscription in Galaxpay => ${err?.message}`); } - if (galaxpayFristTransactionId === GalaxPayTransaction.galaxPayId) { + if (galaxpayFristTransactionId === whGalaxPayTransaction.galaxPayId) { // update frist payment const order = await findOrderById(originalOrderId); // Update value Subscription in GalaxPay - // logger.log('plan-> ', JSON.stringify(plan)); // subscripton is paid if (checkStatusPaid(galaxPayTransactionStatus) && order.items) { const oldSubscriptionValue = docSubscription.data()?.value || ({ ...order.amount }.total * 100); - const newValue = checkAmountItemsOrder( - { ...order.amount }, - [...order.items], - { ...plan }, + let shippingLine: Exclude[number] | undefined; + + if (order.shipping_lines?.length) { + [shippingLine] = order.shipping_lines; + } + + const newValue = await checkItemsAndRecalculeteOrder( + order.amount, + order.items, + plan, + null, + shippingLine, ); - if (newValue && newValue !== oldSubscriptionValue) { - await updateValueSubscription( - app, + if (newValue && (newValue / 100) !== oldSubscriptionValue) { + await checkAndUpdateSubscriptionGalaxpay( originalOrderId, order.amount, order.items, plan, + oldSubscriptionValue, + shippingLine, ); - collectionSubscription.doc(originalOrderId) + await collectionSubscription.doc(originalOrderId) .set({ updatedAt: new Date().toISOString(), value: newValue, @@ -300,16 +353,18 @@ const handleWehook = async (req: Request, res: Response) => { && checkStatusIsEqual(order.financial_status, galaxPayTransactionStatus)) { // check status is equal return res.sendStatus(200); - } if (order.transactions) { + } + + if (order.transactions?.length) { // update payment const transactionId = order.transactions[0]._id; - let notificationCode = `;${GalaxPayTransaction.tid || ''};`; - notificationCode += `${GalaxPayTransaction.authorizationCode || ''}`; + let notificationCode = `;${whGalaxPayTransaction.tid || ''};`; + notificationCode += `${whGalaxPayTransaction.authorizationCode || ''}`; const bodyPaymentHistory = { date_time: transactionCreatedAt || new Date().toISOString(), status: parseStatus(galaxPayTransactionStatus), transaction_id: transactionId, - notification_code: `${type};${galaxpayHook.webhookId}${notificationCode}`, + notification_code: `${type};${whGalaxPay.webhookId}${notificationCode}`, flags: ['GalaxPay'], } as any; // TODO: incompatible type=> amount and status;; @@ -319,8 +374,9 @@ const handleWehook = async (req: Request, res: Response) => { `orders/${order._id}/transactions/${transactionId}`, { intermediator: { - transaction_id: GalaxPayTransaction.tid || '', - transaction_code: GalaxPayTransaction.authorizationCode || '', + transaction_id: whGalaxPayTransaction.galaxPayId, + transaction_reference: whGalaxPayTransaction.tid || '', + transaction_code: whGalaxPayTransaction.authorizationCode || '', }, }, ); @@ -329,29 +385,29 @@ const handleWehook = async (req: Request, res: Response) => { } } else { /* - add order, because recurrence creates all transactions in the - first transaction when quantity is non-zero,search for the order by ID, - if not found, create the transaction, and if found, check if it will be - necessary to update the transaction status - */ - const transactionId = String(gerateId(GalaxPayTransaction.galaxPayId)); + add order, because recurrence creates all transactions in the + first transaction when quantity is non-zero,search for the order by ID, + if not found, create the transaction, and if found, check if it will be + necessary to update the transaction status + */ + const tId = whGalaxPayTransaction.galaxPayId; - const { result } = await findOrderByTransactionId(transactionId); + const { result } = await getOrderIntermediatorTId(tId); if (!result || !result.length) { // logger.log('> Not found Transaction in API') if (checkStatusPaid(galaxPayTransactionStatus) - && checkPayDay(GalaxPayTransaction.payday)) { + && checkPayDay(whGalaxPayTransaction.payday)) { // necessary to create order return createTransaction( res, subscriptionLabel, - plan, + docSubscription, orderNumber, galaxpayFristTransactionId, - GalaxPayTransaction, - GalaxPaySubscription, - GalaxPayTransactionValue, + whGalaxPayTransaction, + whGalaxPaySubscription, + whGalaxPayTransactionValue, originalOrderId, ); } @@ -383,26 +439,31 @@ const handleWehook = async (req: Request, res: Response) => { return res.sendStatus(400); } } else { - logger.log(`>> galaxpay webhook: Status or checkPayDay invalid => Payday: ${GalaxPayTransaction.payday} now: ${new Date().toISOString()}`); + logger.log(`>> galaxpay webhook: Status or checkPayDay invalid => Payday: ${whGalaxPayTransaction.payday} now: ${new Date().toISOString()}`); return res.status(404).send('Status or checkPayDay invalid'); } } - const order = result[0]; + const orderId = result[0]._id; + const { data: order } = await api.get(`orders/${orderId}`); + const transaction = order.transactions?.find( + (transactionFind) => transactionFind.intermediator?.transaction_id === tId, + ); + if (order.financial_status && checkStatusIsEqual(order.financial_status, galaxPayTransactionStatus)) { // check status is equal // logger.log('> Equals Status') return res.sendStatus(200); } - // logger.log('> Order id ') // update payment - let notificationCode = `;${GalaxPayTransaction.tid || ''};`; - notificationCode += `${GalaxPayTransaction.authorizationCode || ''}`; + let notificationCode = `;${whGalaxPayTransaction.tid || ''};`; + notificationCode += `${whGalaxPayTransaction.authorizationCode || ''}`; + const bodyPaymentHistory = { date_time: transactionCreatedAt || new Date().toISOString(), status: parseStatus(galaxPayTransactionStatus), - transaction_id: transactionId, - notification_code: `${type};${galaxpayHook.webhookId}${notificationCode}`, + // transaction_id: transactionId, + notification_code: `${type};${whGalaxPay.webhookId}${notificationCode}`, flags: ['GalaxPay'], } as any; // TODO: incompatible type=> amount and status; @@ -411,11 +472,11 @@ const handleWehook = async (req: Request, res: Response) => { // logger.log('> create Payment History') await api.patch( - `orders/${order._id}/transactions/${transactionId}`, + `orders/${order._id}/transactions/${transaction?._id}`, { intermediator: { - transaction_id: GalaxPayTransaction.tid || '', - transaction_code: GalaxPayTransaction.authorizationCode || '', + transaction_id: whGalaxPayTransaction.tid || '', + transaction_code: whGalaxPayTransaction.authorizationCode || '', }, }, ); @@ -435,42 +496,64 @@ const handleWehook = async (req: Request, res: Response) => { // return res.status(404).send('Document not found in firestore'); } if (type === 'subscription.addTransaction') { - if (GalaxPaySubscriptionQuantity === 0) { - // find transaction in firebase + if (whGalaxPaySubscription?.quantity === 0) { + // console.log('>> Add transaction') const documentSnapshot = await collectionSubscription.doc(originalOrderId).get(); if (documentSnapshot && documentSnapshot.exists) { - const docSubscription = documentSnapshot.data(); - - if (docSubscription) { - const { - galaxpayFristTransactionId, - plan, - orderNumber, - subscriptionLabel, - } = docSubscription.data(); - - if (checkPayDay(GalaxPayTransaction.payday)) { - return createTransaction( - res, - subscriptionLabel, - plan, - orderNumber, - galaxpayFristTransactionId, - GalaxPayTransaction, - GalaxPaySubscription, - GalaxPayTransactionValue, - originalOrderId, - ); + const subscriptionDoc = documentSnapshot.data(); + if (subscriptionDoc) { + const { plan, transactionId } = subscriptionDoc; + if (transactionId !== whGalaxPayTransaction.galaxPayId) { + const order = await findOrderById(originalOrderId); + + let { itemsAndAmount } = subscriptionDoc; + if (itemsAndAmount && itemsAndAmount.items?.length) { + compareDocItemsWithOrder( + itemsAndAmount, + order.items, + order.amount, + whGalaxPayTransactionValue, + ); + } + + let shippingLine: Exclude[number] | undefined; + + if (order?.shipping_lines?.length) { + [shippingLine] = order.shipping_lines; + } + + if (order.items) { + await checkItemsAndRecalculeteOrder( + order.amount, + order.items, + plan, + null, + shippingLine, + ); + } + + itemsAndAmount = createItemsAndAmount(order.amount, order.items); + + await collectionTransactions.doc(`${whGalaxPayTransaction.galaxPayId}`) + .set( + { + itemsAndAmount, + updatedAt: new Date().toISOString(), + }, + { merge: true }, + ); + return res.sendStatus(200); } + // console.log('>> Transaction Original') + return res.sendStatus(200); } } - // - return res.status(404).send('Document not found in firestore'); + + return res.sendStatus(400); } // Avoid retries of this GalaxPay webhook return res.status(200) - .send(`Subscription webhook with non-zero quantity. - The Order will be analyzed with the updateStatus webhook.`); + .send('Subscription webhook with non-zero quantity. The Order will be analyzed with the updateStatus webhook.'); } // return res.status(404).send('Unidentified webhook type'); diff --git a/packages/apps/galaxpay/src/galaxpay-create-transaction.ts b/packages/apps/galaxpay/src/galaxpay-create-transaction.ts index cd3fdab5d..16013330f 100644 --- a/packages/apps/galaxpay/src/galaxpay-create-transaction.ts +++ b/packages/apps/galaxpay/src/galaxpay-create-transaction.ts @@ -54,7 +54,7 @@ export default async (appData: AppModuleBody) => { if (!process.env.GALAXPAY_ID) { const galaxpayId = configApp.galaxpay_id; - if (typeof galaxpayId === 'string' && galaxpayId) { + if (galaxpayId && typeof galaxpayId === 'string') { process.env.GALAXPAY_ID = galaxpayId; } else { logger.warn('Missing GalaxPay ID'); @@ -63,7 +63,7 @@ export default async (appData: AppModuleBody) => { if (!process.env.GALAXPAY_HASH) { const galaxpayHash = configApp.galaxpay_hash; - if (typeof galaxpayHash === 'string' && galaxpayHash) { + if (galaxpayHash && typeof galaxpayHash === 'string') { process.env.GALAXPAY_HASH = galaxpayHash; } else { logger.warn('Missing GalaxPay Hash'); @@ -118,14 +118,14 @@ export default async (appData: AppModuleBody) => { [plan] = configApp.plans; } - const finalAmount = amount.total; + const finalAmount = Math.floor(parseFloat((amount.total).toFixed(2)) * 1000) / 10; const fristPayment = new Date(); - const quantity = plan?.quantity || 0; + const quantity = 0; const galaxpaySubscriptions: GalaxPaySubscriptions = { myId: `${orderId}`, // requered - value: Math.floor(finalAmount * 100), - quantity, // recorrence quantity + value: finalAmount, + quantity, periodicity: parsePeriodicityToGalaxPay(plan?.periodicity) || 'monthly', Customer: galaxpayCustomer, ExtraFields: extraFields, @@ -191,7 +191,7 @@ export default async (appData: AppModuleBody) => { if (type === 'recurrence') { const { data: { Subscription } } = await axios.post('/subscriptions', galaxpaySubscriptions); - logger.log('>(App: GalaxPay) New Subscription ', Subscription, ' <'); + logger.log(`>(App: GalaxPay) New Subscription ${JSON.stringify(Subscription)}`); transaction.payment_link = Subscription.paymentLink; const transactionGalaxPay = Subscription.Transactions[0]; @@ -201,7 +201,8 @@ export default async (appData: AppModuleBody) => { }; transaction.intermediator = { - transaction_id: transactionGalaxPay.tid, + transaction_id: transactionGalaxPay.galaxPayId, + transaction_reference: transactionGalaxPay.tid, transaction_code: transactionGalaxPay.authorizationCode, }; @@ -215,6 +216,7 @@ export default async (appData: AppModuleBody) => { quantity, create_at: new Date().toISOString(), plan, + value: finalAmount, }) .catch(logger.error); } diff --git a/packages/apps/galaxpay/src/galaxpay-list-payments.ts b/packages/apps/galaxpay/src/galaxpay-list-payments.ts index 5d4734549..527b1e68c 100644 --- a/packages/apps/galaxpay/src/galaxpay-list-payments.ts +++ b/packages/apps/galaxpay/src/galaxpay-list-payments.ts @@ -16,7 +16,7 @@ export default async (data: AppModuleBody) => { const { application } = data; const params = data.params as ListPaymentsParams; // https://apx-mods.e-com.plus/api/v1/list_payments/schema.json?store_id=100 - const amount = params.amount || { total: undefined, discount: undefined }; + let amount = { ...params.amount } || { total: undefined, discount: undefined }; // const initialTotalAmount = amount.total; const configApp = { @@ -31,7 +31,7 @@ export default async (data: AppModuleBody) => { if (!process.env.GALAXPAY_ID) { const galaxpayId = configApp.galaxpay_id; - if (typeof galaxpayId === 'string' && galaxpayId) { + if (galaxpayId && typeof galaxpayId === 'string') { process.env.GALAXPAY_ID = galaxpayId; } else { logger.warn('Missing GalaxPay ID'); @@ -40,7 +40,7 @@ export default async (data: AppModuleBody) => { if (!process.env.GALAXPAY_HASH) { const galaxpayHash = configApp.galaxpay_hash; - if (typeof galaxpayHash === 'string' && galaxpayHash) { + if (galaxpayHash && typeof galaxpayHash === 'string') { process.env.GALAXPAY_HASH = galaxpayHash; } else { logger.warn('Missing GalaxPay Hash'); @@ -93,7 +93,7 @@ export default async (data: AppModuleBody) => { const planName = plan.label ? plan.label : 'Plano'; if (type === 'recurrence' && planName) { - label = `${planName} ${periodicity} ${label}`; + label = `${planName}`; } const gateway: Gateway = { label, @@ -101,7 +101,7 @@ export default async (data: AppModuleBody) => { text: methodConfig.text, payment_method: { code: isPix ? 'account_deposit' : paymentMethod as CodePaymentMethod, // pix is defined payment method outher - name: `${label} - ${intermediator.name}`, + name: `${label} - ${periodicity} - ${intermediator.name}`, }, type, intermediator, @@ -126,18 +126,11 @@ export default async (data: AppModuleBody) => { }; } - const planDiscount = discountPlan(plan.discount, amount); - if (planDiscount) { - if (gateway && gateway.discount) { - gateway.discount = planDiscount; - } - - response.discount_option = { - label, - ...planDiscount, - apply_at: planDiscount?.apply_at !== 'freight' - ? planDiscount?.apply_at : undefined, - }; + const handleDiscount = discountPlan(label, plan, amount); + if (handleDiscount) { + amount = handleDiscount.amount; + gateway.discount = handleDiscount.discount; + response.discount_option = handleDiscount.discountOption; } response.payment_gateways.push(gateway); } diff --git a/packages/apps/galaxpay/types/config-app.d.ts b/packages/apps/galaxpay/types/config-app.d.ts index f04949f26..bc1b6240b 100644 --- a/packages/apps/galaxpay/types/config-app.d.ts +++ b/packages/apps/galaxpay/types/config-app.d.ts @@ -31,7 +31,14 @@ export type GalaxpayApp = { periodicity: 'Semanal' | 'Quinzenal' | 'Mensal' | 'Bimestral' | 'Trimestral' | 'Semestral' | 'Anual' quantity: integer | 0, discount: { - percentage: boolean, + type?: 'percentage' | 'fixed', + value: number, + apply_at: 'total' | 'subtotal' | 'frete', + min_amount?: number + }, + discount_first_installment: { + disable?: boolean, + type?: 'percentage' | 'fixed', value: number, apply_at: 'total' | 'subtotal' | 'frete', min_amount?: number