import prisma from "../db"; import config from "../config.json"; import { CustomerType, PayCondition } from "@prisma/client"; if (!process.env.FLOW_ACCOUNT_URL) throw new Error("Require FLOW_ACCOUNT_URL"); if (!process.env.FLOW_ACCOUNT_CLIENT_ID) throw new Error("Require FLOW_ACCOUNT_CLIENT_ID"); if (!process.env.FLOW_ACCOUNT_CLIENT_SECRET) throw new Error("Require FLOW_ACCOUNT_CLIENT_SECRET"); if (!process.env.FLOW_ACCOUNT_CLIENT_GRANT_TYPE) { throw new Error("Require FLOW_ACCOUNT_CLIENT_GRANT_TYPE"); } if (!process.env.FLOW_ACCOUNT_CLIENT_SCOPE) throw new Error("Require FLOW_ACCOUNT_CLIENT_SCOPE"); const api = process.env.FLOW_ACCOUNT_URL; const clientId = process.env.FLOW_ACCOUNT_CLIENT_ID; const clientSecret = process.env.FLOW_ACCOUNT_CLIENT_SECRET; const clientGrantType = process.env.FLOW_ACCOUNT_CLIENT_GRANT_TYPE; const clientScope = process.env.FLOW_ACCOUNT_CLIENT_SCOPE; let token: string = ""; let tokenExpire: number = Date.now(); // float 0.0 - 1.0 const VAT_DEFAULT = config.vat; enum ContactGroup { PERS = 1, CORP = 3, } enum CreditType { Cash = 3, CreditDay = 1, CreditNoLimit = 5, } enum DocumentDeductionType { Special = 1, Agent = 3, Process = 5, Round = 7, } enum SaleAndPurchaseChannel { Lazada = "Lazada", Shopee = "Shopee", } enum ProductAndServiceType { Service = 1, ProductNonInv = 3, ProductInv = 5, } enum PaymentDeductionType { SpecialDiscount = 1, Commission = 3, Process = 5, Round = 7, } type ProductAndService = { type?: ProductAndServiceType; name: string; description?: string; quantity: number; unitName?: string; pricePerUnit: number; total: number; discountAmount?: number; vatRate?: number; sellChartOfAccountCode?: string; buyChartOfAccountcode?: string; }; const flowAccountAPI = { auth: async () => { // Get new token if it is expiring soon (30s). if (token && Date.now() < tokenExpire + 30 * 1000) return { token, tokenExpire }; const payload = { ["client_id"]: clientId, ["client_secret"]: clientSecret, ["grant_type"]: clientGrantType, ["scope"]: clientScope, }; const res = await fetch(api + "/token", { method: "POST", headers: { ["Content-Type"]: "application/x-www-form-urlencoded", }, body: new URLSearchParams(payload), }); if (!res.ok) throw new Error("Cannot connect to flowaccount: " + (await res.text())); const body: { access_token: string; expires_in: number; } = await res.json(); token = body.access_token; tokenExpire = Date.now() + body.expires_in * 1000; return { token, tokenExpire }; }, async createReceipt( data: { recordId?: string; contactCode?: string; contactName: string; contactAddress: string; contactTaxId?: string; contactBranch?: string; contactPerson?: string; contactEmail?: string; contactNumber?: string; contactZipCode?: string; contactGroup?: ContactGroup; /** This must be in yyyy-MM-dd format if pass as string */ publishedOn?: Date | string; creditType?: CreditType; /** This is integer */ creditDays?: number; /** This must be in yyyy-MM-dd format if pass as string */ dueDate?: Date | string; salesName?: string; projectName?: string; reference?: string; isVatInclusive: boolean; useReceiptDeduction?: boolean; subTotal: number; discounPercentage?: number; discountAmount?: number; totalAfterDiscount: number; isVat?: boolean; vatAmount?: number; /** VAT included */ grandTotal?: number; /** แสดงหรือไม่แสดง หัก ณ ที่จ่ายท้ายเอกสาร */ documentShowWithholdingTax?: boolean; /** ภาษีหัก ณ ที่จ่าย (%) */ documentWithholdingTaxPercentage?: number; /** ภาษีหัก ณ ที่จ่าย */ documentWithholdingTaxAmount?: number; documentDeductionType?: DocumentDeductionType; documentDeductionAmount?: number; remarks?: string; internalNotes?: string; showSignatureOrStamp?: boolean; documentStructureType?: "SimpleDocument" | null; saleAndPurchaseChannel?: SaleAndPurchaseChannel; items: ProductAndService[]; /** This must be in yyyy-MM-dd format if pass as string */ // paymentDate: Date | string; // paymentDeductionType?: PaymentDeductionType; // collected: number; }, withPayment?: boolean, ) { const { token } = await flowAccountAPI.auth(); if (data.publishedOn instanceof Date) { let date = data.publishedOn.getDate(); let month = data.publishedOn.getMonth() + 1; let year = data.publishedOn.getFullYear(); data.publishedOn = `${year}-${String(month).padStart(2, "0")}-${String(date).padStart(2, "0")}`; } if (data.dueDate instanceof Date) { let date = data.dueDate.getDate(); let month = data.dueDate.getMonth() + 1; let year = data.dueDate.getFullYear(); data.dueDate = `${year}-${String(month).padStart(2, "0")}-${String(date).padStart(2, "0")}`; } /* if (data.paymentDate instanceof Date) { let date = data.paymentDate.getDate(); let month = data.paymentDate.getMonth() + 1; let year = data.paymentDate.getFullYear(); data.paymentDate = `${year}-${String(month).padStart(2, "0")}-${String(date).padStart(2, "0")}`; } */ const res = await fetch( api + "/upgrade/receipts/inline" + (withPayment ? "/with-payment" : ""), { method: "POST", headers: { ["Content-Type"]: `application/json`, ["Authorization"]: `Bearer ${token}`, }, body: JSON.stringify({ ...data, referenceDocument: [] }), }, ); return { ok: res.ok, status: res.status, body: await res.json(), }; }, async getInvoiceDocument(recordId: string) { const { token } = await flowAccountAPI.auth(); const res = await fetch(api + "/receipts/sharedocument", { method: "POST", headers: { ["Content-Type"]: `application/json`, ["Authorization"]: `Bearer ${token}`, }, body: JSON.stringify({ documentId: +recordId, culture: "th", }), }); return { ok: res.ok, status: res.status, body: await res.json(), }; }, }; const flowAccount = { issueInvoice: async (invoiceId: string) => { const data = await prisma.invoice.findFirst({ where: { id: invoiceId }, include: { installments: true, quotation: { include: { registeredBranch: { include: { province: true, district: true, subDistrict: true, }, }, productServiceList: { include: { worker: true, service: true, work: true, product: true, }, }, customerBranch: { include: { customer: true, province: true, district: true, subDistrict: true }, }, createdBy: true, updatedBy: true, }, }, payment: true, }, }); if (!data) return null; const quotation = data.quotation; const customer = quotation.customerBranch; const product = quotation.payCondition === PayCondition.BillFull || quotation.payCondition === PayCondition.Full ? quotation.productServiceList : quotation.productServiceList.filter((lhs) => data.installments.some((rhs) => rhs.no === lhs.installmentNo), ); const payload = { contactCode: customer.code, contactName: (customer.customer.customerType === CustomerType.PERS ? [customer.firstName, customer.lastName].join(" ").trim() : customer.registerName) || "-", contactAddress: [ customer.address, !!customer.moo ? "หมู่ " + customer.moo : null, !!customer.soi ? "ซอย " + customer.soi : null, !!customer.street ? "ถนน " + customer.street : null, (customer.province?.id === "10" ? "แขวง" : "อำเภอ") + customer.subDistrict?.name, (customer.province?.id === "10" ? "เขต" : "ตำบล") + customer.district?.name, "จังหวัด" + customer.province?.name, customer.subDistrict?.zipCode, ] .filter(Boolean) .join(" "), contactTaxId: customer.citizenId || customer.code, contactBranch: (customer.customer.customerType === CustomerType.PERS ? [customer.firstName, customer.lastName].join(" ").trim() : customer.registerName) || "-", contactPerson: customer.contactName ?? undefined, contactEmail: customer.email, contactNumber: customer.telephoneNo, contactZipCode: customer.subDistrict?.zipCode, contactGroup: customer.customer.customerType === "PERS" ? ContactGroup.PERS : ContactGroup.CORP, dueDate: quotation.dueDate, salesName: [quotation.createdBy?.firstName, quotation.createdBy?.lastName].join(" "), isVatInclusive: true, isVat: true, useReceiptDeduction: false, discounPercentage: 0, discountAmount: quotation.totalDiscount, subTotal: quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom" ? 0 : quotation.totalPrice, totalAfterDiscount: quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom" ? 0 : quotation.finalPrice, vatAmount: quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom" ? 0 : quotation.vat, grandTotal: quotation.payCondition === "BillSplitCustom" || quotation.payCondition === "SplitCustom" ? data.installments.reduce((a, c) => a + c.amount, 0) : quotation.finalPrice, items: product.map((v) => ({ type: ProductAndServiceType.ProductNonInv, name: v.product.name, pricePerUnit: v.pricePerUnit, quantity: v.amount, discountAmount: v.discount, total: (v.pricePerUnit - (v.discount || 0)) * v.amount + v.vat, vatRate: v.vat === 0 ? 0 : Math.round(VAT_DEFAULT * 100), })), }; return await flowAccountAPI.createReceipt(payload, false); }, getInvoiceDocument: async (recordId: string) => { const ret = await flowAccountAPI.getInvoiceDocument(recordId); if (ret && ret.ok) { return ret.body; } return null; }, }; export default flowAccount;