Files
accounted/scripts/seed-export-data.mjs
T
2026-02-24 16:06:36 +01:00

555 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Seed script: populate data for export extensions
*
* Creates EU customers, foreign-currency invoices, and journal entries
* so that all 4 export extensions (EU Sales List, VAT Monitor, Intrastat,
* Currency Receivables) have data to display.
*
* Usage: node scripts/seed-export-data.mjs
*/
import { createClient } from '@supabase/supabase-js'
import 'dotenv/config'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY
if (!supabaseUrl || !serviceRoleKey) {
console.error('Missing NEXT_PUBLIC_SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY in .env')
process.exit(1)
}
const supabase = createClient(supabaseUrl, serviceRoleKey)
// ── Helpers ─────────────────────────────────────────────────
function round2(n) {
return Math.round(n * 100) / 100
}
function randomId() {
return crypto.randomUUID()
}
function today() {
return new Date().toISOString().split('T')[0]
}
function daysAgo(n) {
const d = new Date()
d.setDate(d.getDate() - n)
return d.toISOString().split('T')[0]
}
function daysFromNow(n) {
const d = new Date()
d.setDate(d.getDate() + n)
return d.toISOString().split('T')[0]
}
// ── Main ────────────────────────────────────────────────────
async function main() {
// 1. Find the user
const { data: { users }, error: usersError } = await supabase.auth.admin.listUsers()
if (usersError) {
console.error('Failed to list users:', usersError.message)
process.exit(1)
}
if (users.length === 0) {
console.error('No users found. Please sign up first.')
process.exit(1)
}
const user = users[0]
const userId = user.id
console.log(`Using user: ${user.email} (${userId})`)
// 2. Ensure company settings exist
const { data: company, error: companyError } = await supabase
.from('company_settings')
.select('*')
.eq('user_id', userId)
.single()
if (companyError || !company) {
console.error('No company_settings found. Complete onboarding first.')
process.exit(1)
}
console.log(`Company: ${company.company_name || '(unnamed)'}`)
// 3. Ensure fiscal period exists for current year
const year = new Date().getFullYear()
const periodStart = `${year}-01-01`
const periodEnd = `${year}-12-31`
let { data: fiscalPeriod } = await supabase
.from('fiscal_periods')
.select('*')
.eq('user_id', userId)
.lte('period_start', today())
.gte('period_end', today())
.limit(1)
.single()
if (!fiscalPeriod) {
console.log(`Creating fiscal period for ${year}...`)
const { data: newPeriod, error: periodError } = await supabase
.from('fiscal_periods')
.insert({
id: randomId(),
user_id: userId,
name: `Räkenskapsår ${year}`,
period_start: periodStart,
period_end: periodEnd,
is_closed: false,
})
.select()
.single()
if (periodError) {
console.error('Failed to create fiscal period:', periodError.message)
process.exit(1)
}
fiscalPeriod = newPeriod
}
console.log(`Fiscal period: ${fiscalPeriod.name} (${fiscalPeriod.period_start} ${fiscalPeriod.period_end})`)
// 4. Create EU customers
const customers = [
{
id: randomId(),
user_id: userId,
name: 'TechHaus GmbH',
customer_type: 'eu_business',
email: 'billing@techhaus.de',
country: 'Germany',
org_number: 'HRB 12345',
vat_number: 'DE123456789',
vat_number_validated: true,
default_payment_terms: 30,
address_line1: 'Friedrichstraße 42',
postal_code: '10117',
city: 'Berlin',
},
{
id: randomId(),
user_id: userId,
name: 'Suomen Softworks Oy',
customer_type: 'eu_business',
email: 'invoices@suomensoftworks.fi',
country: 'Finland',
org_number: '1234567-8',
vat_number: 'FI12345678',
vat_number_validated: true,
default_payment_terms: 30,
address_line1: 'Mannerheimintie 10',
postal_code: '00100',
city: 'Helsinki',
},
{
id: randomId(),
user_id: userId,
name: 'Oranje Logistics B.V.',
customer_type: 'eu_business',
email: 'finance@oranjelogistics.nl',
country: 'Netherlands',
org_number: 'KvK 87654321',
vat_number: 'NL123456789B01',
vat_number_validated: true,
default_payment_terms: 14,
address_line1: 'Keizersgracht 120',
postal_code: '1015 AA',
city: 'Amsterdam',
},
]
console.log('\nCreating 3 EU customers...')
const { error: custError } = await supabase.from('customers').insert(customers)
if (custError) {
console.error('Failed to create customers:', custError.message)
process.exit(1)
}
for (const c of customers) {
console.log(` ✓ ${c.name} (${c.vat_number})`)
}
// 5. Create invoices
// Mix of: SEK reverse_charge, EUR reverse_charge, USD
const nextNum = company.next_invoice_number || 1
const invoices = [
// Invoice 1: SEK reverse_charge to German customer (for EU Sales List — goods)
{
id: randomId(),
user_id: userId,
customer_id: customers[0].id,
invoice_number: `${company.invoice_prefix || 'F'}${String(nextNum).padStart(4, '0')}`,
invoice_date: daysAgo(20),
due_date: daysFromNow(10),
status: 'sent',
currency: 'SEK',
document_type: 'invoice',
vat_treatment: 'reverse_charge',
vat_rate: 0,
subtotal: 85000,
subtotal_sek: 85000,
vat_amount: 0,
vat_amount_sek: 0,
total: 85000,
total_sek: 85000,
exchange_rate: null,
moms_ruta: '35',
reverse_charge_text: 'Reverse charge VAT to be accounted for by the recipient according to article 196 Council Directive 2006/112/EC',
},
// Invoice 2: EUR reverse_charge to Finnish customer (for EU Sales List — services + Currency Receivables)
{
id: randomId(),
user_id: userId,
customer_id: customers[1].id,
invoice_number: `${company.invoice_prefix || 'F'}${String(nextNum + 1).padStart(4, '0')}`,
invoice_date: daysAgo(15),
due_date: daysFromNow(15),
status: 'sent',
currency: 'EUR',
document_type: 'invoice',
vat_treatment: 'reverse_charge',
vat_rate: 0,
subtotal: 5000,
subtotal_sek: 57500,
vat_amount: 0,
vat_amount_sek: 0,
total: 5000,
total_sek: 57500,
exchange_rate: 11.50,
exchange_rate_date: daysAgo(15),
moms_ruta: '39',
reverse_charge_text: 'Reverse charge VAT to be accounted for by the recipient according to article 196 Council Directive 2006/112/EC',
},
// Invoice 3: EUR reverse_charge to Dutch customer — goods (for EU Sales List + Currency Receivables)
{
id: randomId(),
user_id: userId,
customer_id: customers[2].id,
invoice_number: `${company.invoice_prefix || 'F'}${String(nextNum + 2).padStart(4, '0')}`,
invoice_date: daysAgo(10),
due_date: daysFromNow(20),
status: 'sent',
currency: 'EUR',
document_type: 'invoice',
vat_treatment: 'reverse_charge',
vat_rate: 0,
subtotal: 12000,
subtotal_sek: 138000,
vat_amount: 0,
vat_amount_sek: 0,
total: 12000,
total_sek: 138000,
exchange_rate: 11.50,
exchange_rate_date: daysAgo(10),
moms_ruta: '35',
reverse_charge_text: 'Reverse charge VAT to be accounted for by the recipient according to article 196 Council Directive 2006/112/EC',
},
// Invoice 4: USD to Dutch customer — overdue (for Currency Receivables)
{
id: randomId(),
user_id: userId,
customer_id: customers[2].id,
invoice_number: `${company.invoice_prefix || 'F'}${String(nextNum + 3).padStart(4, '0')}`,
invoice_date: daysAgo(45),
due_date: daysAgo(15),
status: 'overdue',
currency: 'USD',
document_type: 'invoice',
vat_treatment: 'reverse_charge',
vat_rate: 0,
subtotal: 8500,
subtotal_sek: 91800,
vat_amount: 0,
vat_amount_sek: 0,
total: 8500,
total_sek: 91800,
exchange_rate: 10.80,
exchange_rate_date: daysAgo(45),
moms_ruta: '35',
reverse_charge_text: 'Reverse charge VAT to be accounted for by the recipient according to article 196 Council Directive 2006/112/EC',
},
// Invoice 5: Paid SEK reverse_charge to Finnish customer (for EU Sales List history)
{
id: randomId(),
user_id: userId,
customer_id: customers[1].id,
invoice_number: `${company.invoice_prefix || 'F'}${String(nextNum + 4).padStart(4, '0')}`,
invoice_date: daysAgo(60),
due_date: daysAgo(30),
status: 'paid',
currency: 'SEK',
document_type: 'invoice',
vat_treatment: 'reverse_charge',
vat_rate: 0,
subtotal: 42000,
subtotal_sek: 42000,
vat_amount: 0,
vat_amount_sek: 0,
total: 42000,
total_sek: 42000,
exchange_rate: null,
moms_ruta: '39',
reverse_charge_text: 'Reverse charge VAT to be accounted for by the recipient according to article 196 Council Directive 2006/112/EC',
},
]
console.log('\nCreating 5 invoices...')
const { error: invError } = await supabase.from('invoices').insert(invoices)
if (invError) {
console.error('Failed to create invoices:', invError.message)
process.exit(1)
}
for (const inv of invoices) {
const cust = customers.find(c => c.id === inv.customer_id)
console.log(` ✓ ${inv.invoice_number}${cust.name}${inv.currency} ${inv.total} (${inv.status})`)
}
// 6. Create invoice items
const invoiceItems = [
// Invoice 1 items (SEK goods to Germany)
{ id: randomId(), invoice_id: invoices[0].id, description: 'Industrial sensors batch', quantity: 50, unit: 'st', unit_price: 1200, line_total: 60000, sort_order: 1 },
{ id: randomId(), invoice_id: invoices[0].id, description: 'Installation & calibration', quantity: 10, unit: 'tim', unit_price: 2500, line_total: 25000, sort_order: 2 },
// Invoice 2 items (EUR services to Finland)
{ id: randomId(), invoice_id: invoices[1].id, description: 'Software consulting', quantity: 40, unit: 'tim', unit_price: 125, line_total: 5000, sort_order: 1 },
// Invoice 3 items (EUR goods to Netherlands)
{ id: randomId(), invoice_id: invoices[2].id, description: 'Steel components CN:72163100', quantity: 200, unit: 'st', unit_price: 45, line_total: 9000, sort_order: 1 },
{ id: randomId(), invoice_id: invoices[2].id, description: 'Aluminium fittings CN:76169990', quantity: 100, unit: 'st', unit_price: 30, line_total: 3000, sort_order: 2 },
// Invoice 4 items (USD goods to Netherlands)
{ id: randomId(), invoice_id: invoices[3].id, description: 'Custom machine parts', quantity: 25, unit: 'st', unit_price: 340, line_total: 8500, sort_order: 1 },
// Invoice 5 items (SEK services to Finland)
{ id: randomId(), invoice_id: invoices[4].id, description: 'IT architecture review', quantity: 24, unit: 'tim', unit_price: 1750, line_total: 42000, sort_order: 1 },
]
const { error: itemsError } = await supabase.from('invoice_items').insert(invoiceItems)
if (itemsError) {
console.error('Failed to create invoice items:', itemsError.message)
process.exit(1)
}
console.log(` ✓ ${invoiceItems.length} invoice items created`)
// Update next_invoice_number
await supabase
.from('company_settings')
.update({ next_invoice_number: nextNum + 5 })
.eq('user_id', userId)
// 7. Create journal entries for the invoices (reverse charge: debit 1510, credit 3305/3308)
// These are needed for VAT Monitor and EU Sales List cross-check
const journalEntries = []
const journalLines = []
// Get current max voucher number
const { data: maxVoucher } = await supabase
.from('journal_entries')
.select('voucher_number')
.eq('user_id', userId)
.order('voucher_number', { ascending: false })
.limit(1)
.single()
let voucherNum = (maxVoucher?.voucher_number || 0) + 1
for (const inv of invoices) {
const entryId = randomId()
const totalSEK = inv.total_sek
// Determine revenue account: goods = 3305, services = 3308
// moms_ruta 35 = goods, 39 = services
const revenueAccount = inv.moms_ruta === '35' ? '3305' : '3308'
journalEntries.push({
id: entryId,
user_id: userId,
fiscal_period_id: fiscalPeriod.id,
voucher_number: voucherNum++,
voucher_series: 'A',
entry_date: inv.invoice_date,
description: `Faktura ${inv.invoice_number}${customers.find(c => c.id === inv.customer_id).name}`,
source_type: 'invoice_created',
source_id: inv.id,
status: 'posted',
committed_at: new Date().toISOString(),
})
// Debit 1510 (accounts receivable)
journalLines.push({
id: randomId(),
journal_entry_id: entryId,
account_number: '1510',
debit_amount: round2(totalSEK),
credit_amount: 0,
currency: inv.currency,
amount_in_currency: inv.currency !== 'SEK' ? inv.total : null,
exchange_rate: inv.exchange_rate,
line_description: `Kundfordran ${inv.invoice_number}`,
sort_order: 1,
})
// Credit revenue account (3305 export goods or 3308 EU services)
journalLines.push({
id: randomId(),
journal_entry_id: entryId,
account_number: revenueAccount,
debit_amount: 0,
credit_amount: round2(totalSEK),
currency: 'SEK',
line_description: `Intäkt ${inv.invoice_number}`,
sort_order: 2,
})
}
// Add a payment entry for invoice 5 (paid) — debit 1930, credit 1510
const paymentEntryId = randomId()
journalEntries.push({
id: paymentEntryId,
user_id: userId,
fiscal_period_id: fiscalPeriod.id,
voucher_number: voucherNum++,
voucher_series: 'A',
entry_date: daysAgo(25),
description: `Betalning ${invoices[4].invoice_number} — Suomen Softworks Oy`,
source_type: 'invoice_paid',
source_id: invoices[4].id,
status: 'posted',
committed_at: new Date().toISOString(),
})
journalLines.push({
id: randomId(),
journal_entry_id: paymentEntryId,
account_number: '1930',
debit_amount: 42000,
credit_amount: 0,
currency: 'SEK',
line_description: `Inbetalning ${invoices[4].invoice_number}`,
sort_order: 1,
})
journalLines.push({
id: randomId(),
journal_entry_id: paymentEntryId,
account_number: '1510',
debit_amount: 0,
credit_amount: 42000,
currency: 'SEK',
line_description: `Reglering ${invoices[4].invoice_number}`,
sort_order: 2,
})
// Add a small FX gain entry (for Currency Receivables realized FX)
const fxEntryId = randomId()
journalEntries.push({
id: fxEntryId,
user_id: userId,
fiscal_period_id: fiscalPeriod.id,
voucher_number: voucherNum++,
voucher_series: 'A',
entry_date: daysAgo(25),
description: 'Kursdifferens vid betalning',
source_type: 'invoice_paid',
status: 'posted',
committed_at: new Date().toISOString(),
})
journalLines.push({
id: randomId(),
journal_entry_id: fxEntryId,
account_number: '1930',
debit_amount: 450,
credit_amount: 0,
currency: 'SEK',
line_description: 'Valutavinst',
sort_order: 1,
})
journalLines.push({
id: randomId(),
journal_entry_id: fxEntryId,
account_number: '3960',
debit_amount: 0,
credit_amount: 450,
currency: 'SEK',
line_description: 'Valutakursvinst',
sort_order: 2,
})
console.log(`\nCreating ${journalEntries.length} journal entries...`)
const { error: jeError } = await supabase.from('journal_entries').insert(journalEntries)
if (jeError) {
console.error('Failed to create journal entries:', jeError.message)
process.exit(1)
}
const { error: jlError } = await supabase.from('journal_entry_lines').insert(journalLines)
if (jlError) {
console.error('Failed to create journal entry lines:', jlError.message)
console.error('Cleaning up journal entries...')
await supabase.from('journal_entries').delete().in('id', journalEntries.map(e => e.id))
process.exit(1)
}
for (const je of journalEntries) {
console.log(` ✓ A${je.voucher_number}${je.description}`)
}
// 8. Add Intrastat product metadata via extension_data
console.log('\nCreating Intrastat product registry...')
const extensionId = 'export/intrastat'
const extensionData = [
{
user_id: userId,
extension_id: extensionId,
key: 'product:STEEL-COMP',
value: { description: 'Steel components', cn_code: '72163100', net_weight_kg: 2.4, country_of_origin: 'SE' },
},
{
user_id: userId,
extension_id: extensionId,
key: 'product:ALU-FIT',
value: { description: 'Aluminium fittings', cn_code: '76169990', net_weight_kg: 0.8, country_of_origin: 'SE' },
},
{
user_id: userId,
extension_id: extensionId,
key: 'product:IND-SENSOR',
value: { description: 'Industrial sensors', cn_code: '90318080', net_weight_kg: 0.35, country_of_origin: 'SE' },
},
]
const { error: extError } = await supabase.from('extension_data').insert(extensionData)
if (extError) {
console.error('Warning: Failed to create extension_data (Intrastat products):', extError.message)
console.log(' (Export extensions will still work, just Intrastat product registry will be empty)')
} else {
for (const ed of extensionData) {
console.log(` ✓ ${ed.key}${ed.value.description} (CN: ${ed.value.cn_code})`)
}
}
// Done
console.log('\n════════════════════════════════════════════════════')
console.log(' Seed data created successfully!')
console.log('════════════════════════════════════════════════════')
console.log('\nYou should now see data in:')
console.log(' • Periodisk sammanställning (EU Sales List) — 3 EU customers, 5 invoices')
console.log(' • Exportmoms-monitor (VAT Monitor) — journal entries on 3305/3308')
console.log(' • Intrastat — goods invoices + product registry')
console.log(' • Valutafordringar (Currency Receivables) — 3 open EUR/USD invoices')
console.log('\nSelect the current month/quarter to see the data.')
}
main().catch(err => {
console.error('Unexpected error:', err)
process.exit(1)
})