أدوات الفوترة الجاهزة مناسبة لسير العمل اليدوي. تصبح قيداً حين تحتاج فوترة مدمجة في منتج SaaS، دعماً للعملات المتعددة، امتثالاً لضريبة القيمة المضافة في الخليج، أو متابعة تلقائية للمتأخرات.
لماذا الأنظمة المالية تحتاج افتراضات هندسية مختلفة
افتراضان هندسيان مناسبان في أغلب الأنظمة يُسببان مشاكل حقيقية في أنظمة الفوترة:
الفاصلة العائمة: حساب الأموال بـ float يُدخل أخطاء تقريب في الخانة الرابعة بعد الفاصلة. في الحسابات المالية، هذا يتراكم. نظام SaaS يُعالج 10,000 فاتورة شهرياً سيُظهر فروقاً في الأرصدة تنمو مع الوقت. نوع NUMERIC في PostgreSQL يخزن القيم العشرية الدقيقة بدون خطأ تمثيل. كل عمود مالي يجب أن يكون NUMERIC(15,2).
الاتساق اللاحق في سير عمل المدفوعات يخلق خطر الشحن المزدوج. إذا نقر المستخدم على "ادفع" مرتين ووصل الطلبان قبل تأكيد الدفع الأول، تحتاج ضمانات idempotency على مستوى الفاتورة والدفعة.
مخطط قاعدة البيانات لنظام فوترة متعدد العملات
CREATE TABLE invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
customer_id UUID NOT NULL,
invoice_number TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'draft',
currency CHAR(3) NOT NULL DEFAULT 'USD',
subtotal NUMERIC(15, 2) NOT NULL DEFAULT 0,
tax_amount NUMERIC(15, 2) NOT NULL DEFAULT 0,
total_amount NUMERIC(15, 2) NOT NULL DEFAULT 0,
due_date DATE,
paid_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, invoice_number)
);
CREATE TABLE invoice_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
description TEXT NOT NULL,
quantity NUMERIC(10, 3) NOT NULL,
unit_price NUMERIC(15, 2) NOT NULL,
tax_rate NUMERIC(5, 4) NOT NULL DEFAULT 0,
line_total NUMERIC(15, 2) NOT NULL
);
قيد UNIQUE (tenant_id, invoice_number) يمنع تكرار أرقام الفواتير على مستوى قاعدة البيانات.
توليد أرقام الفواتير بطريقة ذرية
أرقام الفواتير يجب أن تكون متسلسلة، مُحدَّدة النطاق لكل مستأجر، ومُولَّدة بطريقة ذرية:
func (s *InvoiceService) nextInvoiceNumber(ctx context.Context, tx pgx.Tx, tenantID uuid.UUID) (string, error) {
var seq int64
err := tx.QueryRow(ctx, `
INSERT INTO invoice_sequences (tenant_id, last_seq)
VALUES ($1, 1)
ON CONFLICT (tenant_id) DO UPDATE
SET last_seq = invoice_sequences.last_seq + 1
RETURNING last_seq
`, tenantID).Scan(&seq)
if err != nil {
return "", fmt.Errorf("invoice sequence: %w", err)
}
return fmt.Sprintf("INV-%d-%06d", time.Now().Year(), seq), nil
}
هذا يعمل داخل نفس المعاملة التي تُنشئ الفاتورة. إذا فشلت العملية، ينتكس تحديث التسلسل معها. الثغرات في التسلسل مقبولة. تكرار أرقام الفواتير غير مقبول.
احتساب الضريبة لولايات قضائية متعددة
معدلات VAT في دول الخليج متباينة: 15% في السعودية، 5% في الإمارات والبحرين، لا ضريبة في لبنان. الشركات اللبنانية التي تُفوتر عملاء خليجيين تحتاج المعدل الصحيح بناءً على الولاية القضائية للعميل:
CREATE TABLE tax_rules (
country_code CHAR(2) NOT NULL,
tax_name TEXT NOT NULL,
rate NUMERIC(5, 4) NOT NULL,
effective_from DATE NOT NULL,
UNIQUE (country_code, effective_from)
);
INSERT INTO tax_rules (country_code, tax_name, rate, effective_from) VALUES
('SA', 'VAT', 0.1500, '2020-07-01'),
('AE', 'VAT', 0.0500, '2018-01-01'),
('LB', 'No Tax', 0.0000, '2000-01-01');
عمود effective_from يعني أن الفواتير التاريخية تعكس المعدل الذي كان سارياً وقت إصدارها. المعدلات تتغير؛ مراجعة الفواتير التاريخية تتطلب معرفة المعدل المطبق وقت الإصدار.
متابعة الفواتير المتأخرة تلقائياً
المتابعة اليدوية للفواتير غير المدفوعة هي عنق الزجاجة الأكثر شيوعاً في فوترة الشركات SaaS في المنطقة. مهمة خلفية يومية ترسل تذكيرات متدرجة:
var overduePipeline = []OverdueStage{
{MinDays: 1, MaxDays: 6, Tone: "gentle"},
{MinDays: 7, MaxDays: 29, Tone: "firm"},
{MinDays: 30, MaxDays: 9999, Tone: "final"},
}
func (s *InvoiceService) ProcessOverdueNotifications(ctx context.Context) error {
for _, stage := range overduePipeline {
invoices, err := s.db.GetInvoicesOverdueByDayRange(ctx, stage.MinDays, stage.MaxDays)
if err != nil {
return err
}
for _, inv := range invoices {
alreadySent, _ := s.db.WasOverdueNotificationSentToday(ctx, inv.ID, stage.Tone)
if alreadySent {
continue
}
s.notifications.SendOverdueReminder(ctx, inv, stage.Tone)
s.db.RecordOverdueNotification(ctx, inv.ID, stage.Tone, time.Now())
}
}
return nil
}
تحقق الـ idempotency يمنع إرسال تذكيرات متعددة في نفس اليوم إذا عملت المهمة أكثر من مرة.
الدروس الأساسية من الإنتاج
استخدم NUMERIC لجميع الأعمدة المالية من البداية. تحويل أعمدة FLOAT إلى NUMERIC بعد وجود بيانات إنتاج يتطلب ترحيلاً ومراجعة فروق التقريب.
ولّد أرقام الفواتير داخل نفس المعاملة التي تُنشئ الفاتورة. لا تولّد الرقم قبل بدء المعاملة.
خزّن معدل الضريبة على بند الفاتورة وقت الإصدار. المعدلات تتغير؛ الفواتير التاريخية يجب أن تعكس المعدل الذي كان سارياً عند الإصدار.
هل تحتاج نظام فوترة احترافياً مدمجاً في منتجك أو بنية مدفوعات من الصفر؟ تواصل مع فريق Voxire على https://voxire.com/get-a-quote/



