Add account hierarchy framework with migrations, RLS policies, and UI components
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
import type { Database } from '@kit/supabase/database';
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import type { CreateSepaBatchInput, AddSepaItemInput, CreateInvoiceInput } from '../schema/finance.schema';
|
||||
import { generateDirectDebitXml, generateCreditTransferXml, validateIban } from './services/sepa-xml-generator.service';
|
||||
import type { Database } from '@kit/supabase/database';
|
||||
|
||||
import type {
|
||||
CreateSepaBatchInput,
|
||||
AddSepaItemInput,
|
||||
CreateInvoiceInput,
|
||||
} from '../schema/finance.schema';
|
||||
import {
|
||||
generateDirectDebitXml,
|
||||
generateCreditTransferXml,
|
||||
validateIban,
|
||||
} from './services/sepa-xml-generator.service';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
@@ -12,24 +21,38 @@ export function createFinanceApi(client: SupabaseClient<Database>) {
|
||||
return {
|
||||
// --- SEPA Batches ---
|
||||
async listBatches(accountId: string) {
|
||||
const { data, error } = await client.from('sepa_batches').select('*')
|
||||
.eq('account_id', accountId).order('created_at', { ascending: false });
|
||||
const { data, error } = await client
|
||||
.from('sepa_batches')
|
||||
.select('*')
|
||||
.eq('account_id', accountId)
|
||||
.order('created_at', { ascending: false });
|
||||
if (error) throw error;
|
||||
return data ?? [];
|
||||
},
|
||||
|
||||
async getBatch(batchId: string) {
|
||||
const { data, error } = await client.from('sepa_batches').select('*').eq('id', batchId).single();
|
||||
const { data, error } = await client
|
||||
.from('sepa_batches')
|
||||
.select('*')
|
||||
.eq('id', batchId)
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
|
||||
async createBatch(input: CreateSepaBatchInput, userId: string) {
|
||||
const { data, error } = await client.from('sepa_batches').insert({
|
||||
account_id: input.accountId, batch_type: input.batchType,
|
||||
description: input.description, execution_date: input.executionDate,
|
||||
pain_format: input.painFormat, created_by: userId,
|
||||
}).select().single();
|
||||
const { data, error } = await client
|
||||
.from('sepa_batches')
|
||||
.insert({
|
||||
account_id: input.accountId,
|
||||
batch_type: input.batchType,
|
||||
description: input.description,
|
||||
execution_date: input.executionDate,
|
||||
pain_format: input.painFormat,
|
||||
created_by: userId,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
@@ -40,13 +63,21 @@ export function createFinanceApi(client: SupabaseClient<Database>) {
|
||||
throw new Error(`Invalid IBAN: ${input.debtorIban}`);
|
||||
}
|
||||
|
||||
const { data, error } = await client.from('sepa_items').insert({
|
||||
batch_id: input.batchId, member_id: input.memberId,
|
||||
debtor_name: input.debtorName, debtor_iban: input.debtorIban.replace(/\s/g, '').toUpperCase(),
|
||||
debtor_bic: input.debtorBic, amount: input.amount,
|
||||
mandate_id: input.mandateId, mandate_date: input.mandateDate,
|
||||
remittance_info: input.remittanceInfo,
|
||||
}).select().single();
|
||||
const { data, error } = await client
|
||||
.from('sepa_items')
|
||||
.insert({
|
||||
batch_id: input.batchId,
|
||||
member_id: input.memberId,
|
||||
debtor_name: input.debtorName,
|
||||
debtor_iban: input.debtorIban.replace(/\s/g, '').toUpperCase(),
|
||||
debtor_bic: input.debtorBic,
|
||||
amount: input.amount,
|
||||
mandate_id: input.mandateId,
|
||||
mandate_date: input.mandateDate,
|
||||
remittance_info: input.remittanceInfo,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
if (error) throw error;
|
||||
|
||||
// Update batch totals
|
||||
@@ -55,21 +86,34 @@ export function createFinanceApi(client: SupabaseClient<Database>) {
|
||||
},
|
||||
|
||||
async getBatchItems(batchId: string) {
|
||||
const { data, error } = await client.from('sepa_items').select('*')
|
||||
.eq('batch_id', batchId).order('created_at');
|
||||
const { data, error } = await client
|
||||
.from('sepa_items')
|
||||
.select('*')
|
||||
.eq('batch_id', batchId)
|
||||
.order('created_at');
|
||||
if (error) throw error;
|
||||
return data ?? [];
|
||||
},
|
||||
|
||||
async recalculateBatchTotals(batchId: string) {
|
||||
const items = await this.getBatchItems(batchId);
|
||||
const total = items.reduce((sum: number, i: any) => sum + Number(i.amount), 0);
|
||||
await client.from('sepa_batches').update({
|
||||
total_amount: total, item_count: items.length,
|
||||
}).eq('id', batchId);
|
||||
const total = items.reduce(
|
||||
(sum: number, i: any) => sum + Number(i.amount),
|
||||
0,
|
||||
);
|
||||
await client
|
||||
.from('sepa_batches')
|
||||
.update({
|
||||
total_amount: total,
|
||||
item_count: items.length,
|
||||
})
|
||||
.eq('id', batchId);
|
||||
},
|
||||
|
||||
async generateSepaXml(batchId: string, creditor: { name: string; iban: string; bic: string; creditorId: string }) {
|
||||
async generateSepaXml(
|
||||
batchId: string,
|
||||
creditor: { name: string; iban: string; bic: string; creditorId: string },
|
||||
) {
|
||||
const batch = await this.getBatch(batchId);
|
||||
const items = await this.getBatchItems(batchId);
|
||||
|
||||
@@ -89,39 +133,65 @@ export function createFinanceApi(client: SupabaseClient<Database>) {
|
||||
})),
|
||||
};
|
||||
|
||||
const xml = batch.batch_type === 'direct_debit'
|
||||
? generateDirectDebitXml(config)
|
||||
: generateCreditTransferXml(config);
|
||||
const xml =
|
||||
batch.batch_type === 'direct_debit'
|
||||
? generateDirectDebitXml(config)
|
||||
: generateCreditTransferXml(config);
|
||||
|
||||
// Update batch status
|
||||
await client.from('sepa_batches').update({ status: 'ready' }).eq('id', batchId);
|
||||
await client
|
||||
.from('sepa_batches')
|
||||
.update({ status: 'ready' })
|
||||
.eq('id', batchId);
|
||||
|
||||
return xml;
|
||||
},
|
||||
|
||||
// --- Invoices ---
|
||||
async listInvoices(accountId: string, opts?: { status?: string }) {
|
||||
let query = client.from('invoices').select('*').eq('account_id', accountId)
|
||||
let query = client
|
||||
.from('invoices')
|
||||
.select('*')
|
||||
.eq('account_id', accountId)
|
||||
.order('issue_date', { ascending: false });
|
||||
if (opts?.status) query = query.eq('status', opts.status as Database['public']['Enums']['invoice_status']);
|
||||
if (opts?.status)
|
||||
query = query.eq(
|
||||
'status',
|
||||
opts.status as Database['public']['Enums']['invoice_status'],
|
||||
);
|
||||
const { data, error } = await query;
|
||||
if (error) throw error;
|
||||
return data ?? [];
|
||||
},
|
||||
|
||||
async createInvoice(input: CreateInvoiceInput, userId: string) {
|
||||
const subtotal = input.items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0);
|
||||
const subtotal = input.items.reduce(
|
||||
(sum, item) => sum + item.quantity * item.unitPrice,
|
||||
0,
|
||||
);
|
||||
const taxAmount = subtotal * (input.taxRate / 100);
|
||||
const totalAmount = subtotal + taxAmount;
|
||||
|
||||
const { data: invoice, error: invoiceError } = await client.from('invoices').insert({
|
||||
account_id: input.accountId, invoice_number: input.invoiceNumber,
|
||||
member_id: input.memberId, recipient_name: input.recipientName,
|
||||
recipient_address: input.recipientAddress, issue_date: input.issueDate,
|
||||
due_date: input.dueDate, status: 'draft',
|
||||
subtotal, tax_rate: input.taxRate, tax_amount: taxAmount, total_amount: totalAmount,
|
||||
notes: input.notes, created_by: userId,
|
||||
}).select().single();
|
||||
const { data: invoice, error: invoiceError } = await client
|
||||
.from('invoices')
|
||||
.insert({
|
||||
account_id: input.accountId,
|
||||
invoice_number: input.invoiceNumber,
|
||||
member_id: input.memberId,
|
||||
recipient_name: input.recipientName,
|
||||
recipient_address: input.recipientAddress,
|
||||
issue_date: input.issueDate,
|
||||
due_date: input.dueDate,
|
||||
status: 'draft',
|
||||
subtotal,
|
||||
tax_rate: input.taxRate,
|
||||
tax_amount: taxAmount,
|
||||
total_amount: totalAmount,
|
||||
notes: input.notes,
|
||||
created_by: userId,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
if (invoiceError) throw invoiceError;
|
||||
|
||||
// Insert line items
|
||||
@@ -134,17 +204,26 @@ export function createFinanceApi(client: SupabaseClient<Database>) {
|
||||
sort_order: i,
|
||||
}));
|
||||
|
||||
const { error: itemsError } = await client.from('invoice_items').insert(items);
|
||||
const { error: itemsError } = await client
|
||||
.from('invoice_items')
|
||||
.insert(items);
|
||||
if (itemsError) throw itemsError;
|
||||
|
||||
return invoice;
|
||||
},
|
||||
|
||||
async getInvoiceWithItems(invoiceId: string) {
|
||||
const { data, error } = await client.from('invoices').select('*').eq('id', invoiceId).single();
|
||||
const { data, error } = await client
|
||||
.from('invoices')
|
||||
.select('*')
|
||||
.eq('id', invoiceId)
|
||||
.single();
|
||||
if (error) throw error;
|
||||
const { data: items } = await client.from('invoice_items').select('*')
|
||||
.eq('invoice_id', invoiceId).order('sort_order');
|
||||
const { data: items } = await client
|
||||
.from('invoice_items')
|
||||
.select('*')
|
||||
.eq('invoice_id', invoiceId)
|
||||
.order('sort_order');
|
||||
return { ...data, items: items ?? [] };
|
||||
},
|
||||
|
||||
@@ -172,15 +251,21 @@ export function createFinanceApi(client: SupabaseClient<Database>) {
|
||||
.eq('account_id', accountId);
|
||||
if (catError) throw catError;
|
||||
|
||||
const mandateMap = new Map((mandates ?? []).map((m: any) => [m.member_id, m]));
|
||||
const categoryMap = new Map((categories ?? []).map((c: any) => [c.id, Number(c.amount)]));
|
||||
const mandateMap = new Map(
|
||||
(mandates ?? []).map((m: any) => [m.member_id, m]),
|
||||
);
|
||||
const categoryMap = new Map(
|
||||
(categories ?? []).map((c: any) => [c.id, Number(c.amount)]),
|
||||
);
|
||||
|
||||
let addedCount = 0;
|
||||
for (const member of (members ?? []) as any[]) {
|
||||
const mandate = mandateMap.get(member.id);
|
||||
if (!mandate) continue;
|
||||
|
||||
const amount = member.dues_category_id ? categoryMap.get(member.dues_category_id) ?? 0 : 0;
|
||||
const amount = member.dues_category_id
|
||||
? (categoryMap.get(member.dues_category_id) ?? 0)
|
||||
: 0;
|
||||
if (amount <= 0) continue;
|
||||
|
||||
const { error } = await client.from('sepa_items').insert({
|
||||
|
||||
Reference in New Issue
Block a user