diff --git a/src/services/flowaccount.ts b/src/services/flowaccount.ts index a3a02d3..5bdb0c9 100644 --- a/src/services/flowaccount.ts +++ b/src/services/flowaccount.ts @@ -1,3 +1,6 @@ +import prisma from "../db"; +import config from "../config.json"; + 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"); @@ -15,6 +18,9 @@ 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, @@ -52,11 +58,13 @@ type ProductAndService = { unitName?: string; pricePerUnit: number; total: number; + discountAmount?: number; + vatRate?: number; sellChartOfAccountCode?: string; buyChartOfAccountcode?: string; }; -const flowaccount = { +const flowAccountAPI = { auth: async () => { // Get new token if it is expiring soon (30s). if (token && Date.now() < tokenExpire + 30 * 1000) return { token, tokenExpire }; @@ -89,7 +97,7 @@ const flowaccount = { return { token, tokenExpire }; }, - createInvoice: async ( + async createInvoice( data: { recordId?: string; contactCode?: string; @@ -138,8 +146,8 @@ const flowaccount = { items: ProductAndService[]; }, withPayment?: boolean, - ) => { - const { token } = await flowaccount.auth(); + ) { + const { token } = await flowAccountAPI.auth(); if (data.publishedOn instanceof Date) { let date = data.publishedOn.getDate(); @@ -155,7 +163,7 @@ const flowaccount = { data.dueDate = `${year}-${String(month).padStart(2, "0")}-${String(date).padStart(2, "0")}`; } - const res = await fetch(api + "/tax-invoices" + withPayment ? "/with-payment" : "", { + const res = await fetch(api + "/tax-invoices/inline" + withPayment ? "/with-payment" : "", { method: "POST", headers: { ["Content-Type"]: `application/json`, @@ -165,10 +173,136 @@ const flowaccount = { }); return { + ok: res.ok, + status: res.status, + body: await res.json(), + }; + }, + + async getInvoiceDocument(recordId: string) { + const res = await fetch(api + "tax-invoices/shareddocument", { + method: "POST", + headers: { + ["Content-Type"]: `application/json`, + ["Authorization"]: `Bearer ${token}`, + }, + body: JSON.stringify({ + recordId, + culture: "th", + }), + }); + + return { + ok: res.ok, status: res.status, body: await res.json(), }; }, }; -export default flowaccount; +const flowAccount = { + issueInvoiceWithPayment: async (invoiceId: string) => { + const data = await prisma.invoice.findFirst({ + where: { id: invoiceId }, + include: { + 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 branch = quotation.registeredBranch; + const product = quotation.productServiceList; + + return await flowAccountAPI.createInvoice( + { + contactCode: customer.code, + contactName: [customer.firstName, customer.lastName].join(" "), + 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.authorizedName ?? undefined, + contactPerson: branch.contactName ?? undefined, + contactEmail: branch.email, + contactNumber: branch.telephoneNo, + contactZipCode: branch.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: true, + + discounPercentage: 0, + discountAmount: quotation.totalDiscount, + + subTotal: quotation.totalPrice, + totalAfterDiscount: quotation.finalPrice, + vatAmount: quotation.vat, + grandTotal: 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: VAT_DEFAULT * 100, + })), + }, + true, + ); + }, + + getInvoiceDocument: async (recordId: string) => { + const ret = await flowAccountAPI.getInvoiceDocument(recordId); + + if (ret && ret.ok) { + return ret.body.data.link; + } + + return null; + }, +}; + +export default flowAccount;