Initial state for GitNexus analysis

This commit is contained in:
Zaid Marzguioui
2026-03-29 19:44:57 +02:00
parent 9d7c7f8030
commit 61ff48cb73
155 changed files with 23483 additions and 1722 deletions

View File

@@ -0,0 +1,154 @@
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';
/* eslint-disable @typescript-eslint/no-explicit-any */
export function createFinanceApi(client: SupabaseClient<Database>) {
const db = client;
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 });
if (error) throw error;
return data ?? [];
},
async getBatch(batchId: string) {
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();
if (error) throw error;
return data;
},
async addItem(input: AddSepaItemInput) {
// Validate IBAN
if (!validateIban(input.debtorIban)) {
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();
if (error) throw error;
// Update batch totals
await this.recalculateBatchTotals(input.batchId);
return data;
},
async getBatchItems(batchId: string) {
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);
},
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);
const config = {
messageId: `MSG-${batchId.slice(0, 8)}-${Date.now()}`,
creationDateTime: new Date().toISOString(),
executionDate: batch.execution_date,
creditor,
transactions: items.map((item: any) => ({
debtorName: item.debtor_name,
debtorIban: item.debtor_iban,
debtorBic: item.debtor_bic,
amount: Number(item.amount),
mandateId: item.mandate_id || `MNDT-${item.id.slice(0, 8)}`,
mandateDate: item.mandate_date || batch.execution_date,
remittanceInfo: item.remittance_info || 'SEPA Einzug',
})),
};
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);
return xml;
},
// --- Invoices ---
async listInvoices(accountId: string, opts?: { status?: string }) {
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']);
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 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();
if (invoiceError) throw invoiceError;
// Insert line items
const items = input.items.map((item, i) => ({
invoice_id: invoice.id,
description: item.description,
quantity: item.quantity,
unit_price: item.unitPrice,
total_price: item.quantity * item.unitPrice,
sort_order: i,
}));
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();
if (error) throw error;
const { data: items } = await client.from('invoice_items').select('*')
.eq('invoice_id', invoiceId).order('sort_order');
return { ...data, items: items ?? [] };
},
// --- Utilities ---
validateIban,
};
}