| import { and, desc, eq, sql } from "drizzle-orm"; |
| import type { Database } from "../client"; |
| import { invoiceTemplates } from "../schema"; |
|
|
| type InvoiceTemplateParams = { |
| customerLabel?: string; |
| title?: string; |
| fromLabel?: string; |
| invoiceNoLabel?: string; |
| issueDateLabel?: string; |
| dueDateLabel?: string; |
| descriptionLabel?: string; |
| priceLabel?: string; |
| quantityLabel?: string; |
| totalLabel?: string; |
| totalSummaryLabel?: string; |
| vatLabel?: string; |
| subtotalLabel?: string; |
| taxLabel?: string; |
| discountLabel?: string; |
| sendCopy?: boolean; |
| paymentLabel?: string; |
| noteLabel?: string; |
| logoUrl?: string | null; |
| currency?: string; |
| paymentDetails?: string | null; |
| fromDetails?: string | null; |
| noteDetails?: string | null; |
| dateFormat?: string; |
| includeVat?: boolean; |
| includeTax?: boolean; |
| includeDiscount?: boolean; |
| includeDecimals?: boolean; |
| includeUnits?: boolean; |
| includeQr?: boolean; |
| includeLineItemTax?: boolean; |
| lineItemTaxLabel?: string; |
| taxRate?: number | null; |
| vatRate?: number | null; |
| size?: "a4" | "letter"; |
| deliveryType?: "create" | "create_and_send" | "scheduled"; |
| includePdf?: boolean; |
| paymentEnabled?: boolean; |
| paymentTermsDays?: number; |
| }; |
|
|
| type CreateInvoiceTemplateParams = { |
| teamId: string; |
| name: string; |
| isDefault?: boolean; |
| } & InvoiceTemplateParams; |
|
|
| type UpsertInvoiceTemplateParams = { |
| id?: string; |
| teamId: string; |
| name?: string; |
| } & InvoiceTemplateParams; |
|
|
| |
| const templateSelectFields = { |
| id: invoiceTemplates.id, |
| name: invoiceTemplates.name, |
| isDefault: invoiceTemplates.isDefault, |
| customerLabel: invoiceTemplates.customerLabel, |
| fromLabel: invoiceTemplates.fromLabel, |
| invoiceNoLabel: invoiceTemplates.invoiceNoLabel, |
| issueDateLabel: invoiceTemplates.issueDateLabel, |
| dueDateLabel: invoiceTemplates.dueDateLabel, |
| descriptionLabel: invoiceTemplates.descriptionLabel, |
| priceLabel: invoiceTemplates.priceLabel, |
| quantityLabel: invoiceTemplates.quantityLabel, |
| totalLabel: invoiceTemplates.totalLabel, |
| vatLabel: invoiceTemplates.vatLabel, |
| taxLabel: invoiceTemplates.taxLabel, |
| paymentLabel: invoiceTemplates.paymentLabel, |
| noteLabel: invoiceTemplates.noteLabel, |
| logoUrl: invoiceTemplates.logoUrl, |
| currency: invoiceTemplates.currency, |
| subtotalLabel: invoiceTemplates.subtotalLabel, |
| paymentDetails: invoiceTemplates.paymentDetails, |
| fromDetails: invoiceTemplates.fromDetails, |
| noteDetails: invoiceTemplates.noteDetails, |
| size: invoiceTemplates.size, |
| dateFormat: invoiceTemplates.dateFormat, |
| includeVat: invoiceTemplates.includeVat, |
| includeTax: invoiceTemplates.includeTax, |
| taxRate: invoiceTemplates.taxRate, |
| deliveryType: invoiceTemplates.deliveryType, |
| discountLabel: invoiceTemplates.discountLabel, |
| includeDiscount: invoiceTemplates.includeDiscount, |
| includeDecimals: invoiceTemplates.includeDecimals, |
| includeQr: invoiceTemplates.includeQr, |
| includeLineItemTax: invoiceTemplates.includeLineItemTax, |
| lineItemTaxLabel: invoiceTemplates.lineItemTaxLabel, |
| totalSummaryLabel: invoiceTemplates.totalSummaryLabel, |
| title: invoiceTemplates.title, |
| vatRate: invoiceTemplates.vatRate, |
| includeUnits: invoiceTemplates.includeUnits, |
| includePdf: invoiceTemplates.includePdf, |
| sendCopy: invoiceTemplates.sendCopy, |
| paymentEnabled: invoiceTemplates.paymentEnabled, |
| paymentTermsDays: invoiceTemplates.paymentTermsDays, |
| emailSubject: invoiceTemplates.emailSubject, |
| emailHeading: invoiceTemplates.emailHeading, |
| emailBody: invoiceTemplates.emailBody, |
| emailButtonText: invoiceTemplates.emailButtonText, |
| }; |
|
|
| |
| |
| |
| export async function getInvoiceTemplates(db: Database, teamId: string) { |
| return db |
| .select(templateSelectFields) |
| .from(invoiceTemplates) |
| .where(eq(invoiceTemplates.teamId, teamId)) |
| .orderBy(desc(invoiceTemplates.isDefault), invoiceTemplates.name); |
| } |
|
|
| |
| |
| |
| export async function getInvoiceTemplateById( |
| db: Database, |
| params: { id: string; teamId: string }, |
| ) { |
| const [result] = await db |
| .select(templateSelectFields) |
| .from(invoiceTemplates) |
| .where( |
| and( |
| eq(invoiceTemplates.id, params.id), |
| eq(invoiceTemplates.teamId, params.teamId), |
| ), |
| ) |
| .limit(1); |
|
|
| return result; |
| } |
|
|
| |
| |
| |
| export async function getInvoiceTemplate(db: Database, teamId: string) { |
| |
| const [defaultTemplate] = await db |
| .select(templateSelectFields) |
| .from(invoiceTemplates) |
| .where( |
| and( |
| eq(invoiceTemplates.teamId, teamId), |
| eq(invoiceTemplates.isDefault, true), |
| ), |
| ) |
| .limit(1); |
|
|
| if (defaultTemplate) { |
| return defaultTemplate; |
| } |
|
|
| |
| const [firstTemplate] = await db |
| .select(templateSelectFields) |
| .from(invoiceTemplates) |
| .where(eq(invoiceTemplates.teamId, teamId)) |
| .orderBy(invoiceTemplates.createdAt) |
| .limit(1); |
|
|
| return firstTemplate; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function createInvoiceTemplate( |
| db: Database, |
| params: CreateInvoiceTemplateParams, |
| ) { |
| const { teamId, name, isDefault, ...rest } = params; |
|
|
| return db.transaction(async (tx) => { |
| |
| const existingTemplates = await tx |
| .select({ id: invoiceTemplates.id }) |
| .from(invoiceTemplates) |
| .where(eq(invoiceTemplates.teamId, teamId)) |
| .limit(1); |
|
|
| const isFirstTemplate = existingTemplates.length === 0; |
|
|
| |
| if (isDefault || isFirstTemplate) { |
| await tx |
| .update(invoiceTemplates) |
| .set({ isDefault: false }) |
| .where(eq(invoiceTemplates.teamId, teamId)); |
| } |
|
|
| |
| const shouldBeDefault = isFirstTemplate || (isDefault ?? false); |
|
|
| const [result] = await tx |
| .insert(invoiceTemplates) |
| .values({ |
| teamId, |
| name, |
| isDefault: shouldBeDefault, |
| ...rest, |
| }) |
| .returning(); |
|
|
| return result; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function upsertInvoiceTemplate( |
| db: Database, |
| params: UpsertInvoiceTemplateParams, |
| ) { |
| const { id, teamId, ...rest } = params; |
|
|
| |
| if (id) { |
| const [result] = await db |
| .update(invoiceTemplates) |
| .set(rest) |
| .where( |
| and(eq(invoiceTemplates.id, id), eq(invoiceTemplates.teamId, teamId)), |
| ) |
| .returning(templateSelectFields); |
|
|
| return result; |
| } |
|
|
| |
| return db.transaction(async (tx) => { |
| |
| const [defaultTemplate] = await tx |
| .select(templateSelectFields) |
| .from(invoiceTemplates) |
| .where( |
| and( |
| eq(invoiceTemplates.teamId, teamId), |
| eq(invoiceTemplates.isDefault, true), |
| ), |
| ) |
| .limit(1); |
|
|
| if (defaultTemplate) { |
| |
| const [result] = await tx |
| .update(invoiceTemplates) |
| .set(rest) |
| .where( |
| and( |
| eq(invoiceTemplates.id, defaultTemplate.id), |
| eq(invoiceTemplates.teamId, teamId), |
| ), |
| ) |
| .returning(templateSelectFields); |
|
|
| return result; |
| } |
|
|
| |
| const [firstTemplate] = await tx |
| .select(templateSelectFields) |
| .from(invoiceTemplates) |
| .where(eq(invoiceTemplates.teamId, teamId)) |
| .orderBy(invoiceTemplates.createdAt) |
| .limit(1); |
|
|
| if (firstTemplate) { |
| |
| const [result] = await tx |
| .update(invoiceTemplates) |
| .set(rest) |
| .where( |
| and( |
| eq(invoiceTemplates.id, firstTemplate.id), |
| eq(invoiceTemplates.teamId, teamId), |
| ), |
| ) |
| .returning(templateSelectFields); |
|
|
| return result; |
| } |
|
|
| |
| const [result] = await tx |
| .insert(invoiceTemplates) |
| .values({ |
| teamId, |
| name: "Default", |
| isDefault: true, |
| ...rest, |
| }) |
| .returning(templateSelectFields); |
|
|
| return result; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| export async function setDefaultTemplate( |
| db: Database, |
| params: { id: string; teamId: string }, |
| ) { |
| return db.transaction(async (tx) => { |
| |
| const [targetTemplate] = await tx |
| .select({ id: invoiceTemplates.id }) |
| .from(invoiceTemplates) |
| .where( |
| and( |
| eq(invoiceTemplates.id, params.id), |
| eq(invoiceTemplates.teamId, params.teamId), |
| ), |
| ) |
| .limit(1); |
|
|
| if (!targetTemplate) { |
| |
| |
| throw new Error("Template not found"); |
| } |
|
|
| |
| await tx |
| .update(invoiceTemplates) |
| .set({ isDefault: false }) |
| .where(eq(invoiceTemplates.teamId, params.teamId)); |
|
|
| |
| const [result] = await tx |
| .update(invoiceTemplates) |
| .set({ isDefault: true }) |
| .where( |
| and( |
| eq(invoiceTemplates.id, params.id), |
| eq(invoiceTemplates.teamId, params.teamId), |
| ), |
| ) |
| .returning(templateSelectFields); |
|
|
| return result; |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function deleteInvoiceTemplate( |
| db: Database, |
| params: { id: string; teamId: string }, |
| ) { |
| return db.transaction(async (tx) => { |
| |
| const [templateToDelete] = await tx |
| .select({ |
| id: invoiceTemplates.id, |
| isDefault: invoiceTemplates.isDefault, |
| }) |
| .from(invoiceTemplates) |
| .where( |
| and( |
| eq(invoiceTemplates.id, params.id), |
| eq(invoiceTemplates.teamId, params.teamId), |
| ), |
| ) |
| .limit(1); |
|
|
| if (!templateToDelete) { |
| throw new Error("Template not found"); |
| } |
|
|
| |
| |
| const otherTemplates = await tx |
| .select({ id: invoiceTemplates.id }) |
| .from(invoiceTemplates) |
| .where( |
| and( |
| eq(invoiceTemplates.teamId, params.teamId), |
| |
| sql`${invoiceTemplates.id} != ${params.id}`, |
| ), |
| ) |
| .limit(1); |
|
|
| if (otherTemplates.length === 0) { |
| throw new Error("Cannot delete the last template"); |
| } |
|
|
| const wasDefault = templateToDelete.isDefault; |
|
|
| |
| const [deleted] = await tx |
| .delete(invoiceTemplates) |
| .where( |
| and( |
| eq(invoiceTemplates.id, params.id), |
| eq(invoiceTemplates.teamId, params.teamId), |
| ), |
| ) |
| .returning(); |
|
|
| |
| let newDefault = null; |
| if (wasDefault) { |
| const [firstRemaining] = await tx |
| .select(templateSelectFields) |
| .from(invoiceTemplates) |
| .where(eq(invoiceTemplates.teamId, params.teamId)) |
| .orderBy(invoiceTemplates.createdAt) |
| .limit(1); |
|
|
| if (firstRemaining) { |
| await tx |
| .update(invoiceTemplates) |
| .set({ isDefault: true }) |
| .where(eq(invoiceTemplates.id, firstRemaining.id)); |
|
|
| newDefault = { ...firstRemaining, isDefault: true }; |
| } |
| } else { |
| |
| |
| const [defaultTemplate] = await tx |
| .select(templateSelectFields) |
| .from(invoiceTemplates) |
| .where( |
| and( |
| eq(invoiceTemplates.teamId, params.teamId), |
| eq(invoiceTemplates.isDefault, true), |
| ), |
| ) |
| .limit(1); |
|
|
| newDefault = defaultTemplate ?? null; |
| } |
|
|
| return { deleted, newDefault }; |
| }); |
| } |
|
|
| |
| |
| |
| export async function getInvoiceTemplateCount(db: Database, teamId: string) { |
| const result = await db |
| .select({ id: invoiceTemplates.id }) |
| .from(invoiceTemplates) |
| .where(eq(invoiceTemplates.teamId, teamId)); |
|
|
| return result.length; |
| } |
|
|