From 84aaa4baff752a734f0d48ed40af89a7ff344c35 Mon Sep 17 00:00:00 2001 From: Rihards Simanovics Date: Mon, 13 Apr 2026 22:56:34 +0000 Subject: [PATCH] feat: add dev database seeder --- database/seeders/DevDataSeeder.php | 473 +++++++++++++++++++++++++++++ 1 file changed, 473 insertions(+) create mode 100644 database/seeders/DevDataSeeder.php diff --git a/database/seeders/DevDataSeeder.php b/database/seeders/DevDataSeeder.php new file mode 100644 index 00000000..d71c866b --- /dev/null +++ b/database/seeders/DevDataSeeder.php @@ -0,0 +1,473 @@ + 'Admin User', + 'email' => 'admin@invoiceshelf.com', + 'role' => 'super admin', + 'password' => 'password', + ]); + + $company = Company::create([ + 'name' => 'Acme Corp', + 'owner_id' => $admin->id, + 'slug' => 'acme-corp', + ]); + + $company->unique_hash = Hashids::connection(Company::class)->encode($company->id); + $company->save(); + $company->setupDefaultData(); // roles, payment methods, units, default settings + + $admin->companies()->attach($company->id); + BouncerFacade::scope()->to($company->id); + $admin->assign('super admin'); + + $admin->setSettings([ + 'language' => 'en', + ]); + + CompanySetting::setSettings([ + 'currency' => 4, // USD + 'time_zone' => 'UTC', + 'language' => 'en', + 'fiscal_year' => '1-12', + 'tax_per_item' => 'NO', + 'discount_per_item' => 'NO', + ], $company->id); + + Setting::setSetting('profile_complete', 'COMPLETED'); + InstallUtils::setCurrentVersion(); + + $companyId = $company->id; + + // ── 2. Extra staff users ────────────────────────────────────────────── + + foreach ([ + ['name' => 'Jane Smith', 'email' => 'jane@invoiceshelf.com'], + ['name' => 'Bob Johnson', 'email' => 'bob@invoiceshelf.com'], + ] as $data) { + $staffUser = User::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'role' => 'admin', + 'password' => 'password', + ]); + $staffUser->companies()->attach($companyId); + BouncerFacade::scope()->to($companyId); + $staffUser->assign('admin'); + } + + // ── 3. Tax types ────────────────────────────────────────────────────── + + $vat = TaxType::create([ + 'name' => 'VAT', + 'calculation_type' => 'percentage', + 'company_id' => $companyId, + 'percent' => 20, + 'description' => 'Value Added Tax (20%)', + 'compound_tax' => 0, + 'collective_tax' => 0, + ]); + + $gst = TaxType::create([ + 'name' => 'GST', + 'calculation_type' => 'percentage', + 'company_id' => $companyId, + 'percent' => 10, + 'description' => 'Goods and Services Tax (10%)', + 'compound_tax' => 0, + 'collective_tax' => 0, + ]); + + // ── 4. Units & catalogue items ──────────────────────────────────────── + + $unit = Unit::where('company_id', $companyId)->first() + ?? Unit::factory()->create(['company_id' => $companyId]); + + $itemData = [ + ['name' => 'Web Design', 'price' => 150000, 'description' => 'Custom website design'], + ['name' => 'Logo Design', 'price' => 50000, 'description' => 'Brand logo design'], + ['name' => 'SEO Audit', 'price' => 80000, 'description' => 'Full site SEO audit'], + ['name' => 'Monthly Hosting', 'price' => 2000, 'description' => 'Shared hosting plan'], + ['name' => 'Content Writing', 'price' => 10000, 'description' => 'Per 1 000 words'], + ['name' => 'Social Media Package', 'price' => 60000, 'description' => 'Monthly social management'], + ['name' => 'Email Marketing', 'price' => 35000, 'description' => 'Campaign design & send'], + ['name' => 'CRM Integration', 'price' => 120000, 'description' => 'Third-party CRM setup'], + ['name' => 'Mobile App Dev', 'price' => 500000, 'description' => 'iOS/Android application'], + ['name' => 'E-Commerce Setup', 'price' => 200000, 'description' => 'Full shop configuration'], + ['name' => 'Domain Registration', 'price' => 1500, 'description' => 'Annual domain fee'], + ['name' => 'SSL Certificate', 'price' => 5000, 'description' => 'Annual SSL certificate'], + ['name' => 'Google Ads Management', 'price' => 45000, 'description' => 'PPC campaign management'], + ['name' => 'Photography Session', 'price' => 30000, 'description' => 'Half-day studio shoot'], + ['name' => 'Video Production', 'price' => 250000, 'description' => 'Corporate promo video'], + ]; + + $items = collect($itemData)->map(fn ($d) => Item::create([ + 'name' => $d['name'], + 'description' => $d['description'], + 'price' => $d['price'], + 'company_id' => $companyId, + 'unit_id' => $unit->id, + 'creator_id' => $admin->id, + 'currency_id' => 1, + 'tax_per_item' => false, + ])); + + // ── 5. Customers ────────────────────────────────────────────────────── + + $customers = Customer::factory()->count(10)->create(['company_id' => $companyId]); + + // ── 6. Invoices ─────────────────────────────────────────────────────── + + $invoiceStatuses = [ + Invoice::STATUS_DRAFT, + Invoice::STATUS_DRAFT, + Invoice::STATUS_SENT, + Invoice::STATUS_SENT, + Invoice::STATUS_VIEWED, + Invoice::STATUS_VIEWED, + Invoice::STATUS_COMPLETED, + Invoice::STATUS_COMPLETED, + Invoice::STATUS_UNPAID, + Invoice::STATUS_UNPAID, + Invoice::STATUS_UNPAID, + Invoice::STATUS_PARTIALLY_PAID, + Invoice::STATUS_PARTIALLY_PAID, + Invoice::STATUS_PAID, + Invoice::STATUS_PAID, + Invoice::STATUS_PAID, + Invoice::STATUS_PAID, + Invoice::STATUS_PAID, + Invoice::STATUS_PAID, + Invoice::STATUS_PAID, + ]; + + $invoices = collect($invoiceStatuses)->map(function (string $status, int $index) use ($companyId, $customers, $items) { + $customer = $customers->random(); + $lineItems = $items->random(rand(1, 3)); + $subTotal = $lineItems->sum('price'); + $tax = (int) ($subTotal * 0.10); + $total = $subTotal + $tax; + $paidStatus = match ($status) { + Invoice::STATUS_PAID => Invoice::STATUS_PAID, + Invoice::STATUS_PARTIALLY_PAID => Invoice::STATUS_PARTIALLY_PAID, + default => Invoice::STATUS_UNPAID, + }; + $dueAmount = match ($paidStatus) { + Invoice::STATUS_PAID => 0, + Invoice::STATUS_PARTIALLY_PAID => (int) ($total / 2), + default => $total, + }; + + $seq = (new SerialNumberFormatter) + ->setModel(new Invoice) + ->setCompany($companyId) + ->setNextNumbers(); + + $invoice = Invoice::create([ + 'invoice_number' => $seq->getNextNumber(), + 'sequence_number' => $seq->nextSequenceNumber, + 'customer_sequence_number' => $seq->nextCustomerSequenceNumber, + 'reference_number' => 'REF-'.str_pad($index + 1, 4, '0', STR_PAD_LEFT), + 'invoice_date' => now()->subDays(rand(1, 120))->toDateString(), + 'due_date' => now()->addDays(rand(1, 30))->toDateString(), + 'status' => $status, + 'paid_status' => $paidStatus, + 'template_name' => 'invoice1', + 'sub_total' => $subTotal, + 'tax' => $tax, + 'total' => $total, + 'due_amount' => $dueAmount, + 'discount' => 0, + 'discount_val' => 0, + 'discount_type' => 'fixed', + 'tax_per_item' => 'NO', + 'tax_included' => false, + 'discount_per_item' => 'NO', + 'notes' => 'Thank you for your business.', + 'unique_hash' => str_random(60), + 'company_id' => $companyId, + 'customer_id' => $customer->id, + 'currency_id' => 1, + 'exchange_rate' => 1, + 'base_sub_total' => $subTotal, + 'base_tax' => $tax, + 'base_total' => $total, + 'base_discount_val' => 0, + 'base_due_amount' => $dueAmount, + ]); + + foreach ($lineItems as $item) { + InvoiceItem::create([ + 'invoice_id' => $invoice->id, + 'item_id' => $item->id, + 'name' => $item->name, + 'description' => $item->description, + 'price' => $item->price, + 'quantity' => 1, + 'total' => $item->price, + 'tax' => 0, + 'discount' => 0, + 'discount_val' => 0, + 'discount_type' => 'fixed', + 'company_id' => $companyId, + 'exchange_rate' => 1, + 'base_price' => $item->price, + 'base_total' => $item->price, + 'base_discount_val' => 0, + 'base_tax' => 0, + ]); + } + + return $invoice; + }); + + // ── 7. Estimates ────────────────────────────────────────────────────── + + $estimateStatuses = [ + Estimate::STATUS_DRAFT, + Estimate::STATUS_DRAFT, + Estimate::STATUS_DRAFT, + Estimate::STATUS_SENT, + Estimate::STATUS_SENT, + Estimate::STATUS_VIEWED, + Estimate::STATUS_VIEWED, + Estimate::STATUS_ACCEPTED, + Estimate::STATUS_ACCEPTED, + Estimate::STATUS_ACCEPTED, + Estimate::STATUS_REJECTED, + Estimate::STATUS_REJECTED, + Estimate::STATUS_EXPIRED, + Estimate::STATUS_EXPIRED, + Estimate::STATUS_EXPIRED, + ]; + + collect($estimateStatuses)->each(function (string $status, int $index) use ($companyId, $customers, $items) { + $customer = $customers->random(); + $lineItems = $items->random(rand(1, 3)); + $subTotal = $lineItems->sum('price'); + $tax = (int) ($subTotal * 0.10); + $total = $subTotal + $tax; + + $seq = (new SerialNumberFormatter) + ->setModel(new Estimate) + ->setCompany($companyId) + ->setNextNumbers(); + + $estimate = Estimate::create([ + 'estimate_number' => $seq->getNextNumber(), + 'sequence_number' => $seq->nextSequenceNumber, + 'customer_sequence_number' => $seq->nextCustomerSequenceNumber, + 'reference_number' => 'EREF-'.str_pad($index + 1, 4, '0', STR_PAD_LEFT), + 'estimate_date' => now()->subDays(rand(1, 90))->toDateString(), + 'expiry_date' => now()->addDays(rand(15, 60))->toDateString(), + 'status' => $status, + 'template_name' => 'estimate1', + 'sub_total' => $subTotal, + 'tax' => $tax, + 'total' => $total, + 'discount' => 0, + 'discount_val' => 0, + 'discount_type' => 'fixed', + 'tax_per_item' => 'NO', + 'tax_included' => false, + 'discount_per_item' => 'NO', + 'notes' => 'This estimate is valid for 30 days.', + 'unique_hash' => str_random(60), + 'company_id' => $companyId, + 'customer_id' => $customer->id, + 'currency_id' => 1, + 'exchange_rate' => 1, + 'base_sub_total' => $subTotal, + 'base_tax' => $tax, + 'base_total' => $total, + 'base_discount_val' => 0, + ]); + + foreach ($lineItems as $item) { + EstimateItem::create([ + 'estimate_id' => $estimate->id, + 'item_id' => $item->id, + 'name' => $item->name, + 'description' => $item->description, + 'price' => $item->price, + 'quantity' => 1, + 'total' => $item->price, + 'tax' => 0, + 'discount' => 0, + 'discount_val' => 0, + 'discount_type' => 'fixed', + 'company_id' => $companyId, + 'exchange_rate' => 1, + 'base_price' => $item->price, + 'base_total' => $item->price, + 'base_discount_val' => 0, + 'base_tax' => 0, + ]); + } + }); + + // ── 8. Payments (linked to paid/partially-paid invoices) ────────────── + + $paymentMethod = DB::table('payment_methods') + ->where('company_id', $companyId) + ->value('id'); + + $paidInvoices = $invoices->filter(fn ($inv) => in_array($inv->paid_status, [ + Invoice::STATUS_PAID, + Invoice::STATUS_PARTIALLY_PAID, + ])); + + $paidInvoices->take(10)->each(function (Invoice $invoice) use ($companyId, $paymentMethod) { + $amount = $invoice->paid_status === Invoice::STATUS_PAID + ? $invoice->total + : (int) ($invoice->total / 2); + + $seq = (new SerialNumberFormatter) + ->setModel(new Payment) + ->setCompany($companyId) + ->setNextNumbers(); + + Payment::create([ + 'payment_number' => $seq->getNextNumber(), + 'sequence_number' => $seq->nextSequenceNumber, + 'customer_sequence_number' => $seq->nextCustomerSequenceNumber, + 'payment_date' => now()->subDays(rand(1, 60))->toDateString(), + 'amount' => $amount, + 'base_amount' => $amount, + 'notes' => 'Payment received. Thank you!', + 'unique_hash' => str_random(60), + 'company_id' => $companyId, + 'customer_id' => $invoice->customer_id, + 'invoice_id' => $invoice->id, + 'payment_method_id' => $paymentMethod, + 'currency_id' => 1, + 'exchange_rate' => 1, + ]); + }); + + // ── 9. Expense categories & expenses ────────────────────────────────── + + $categoryNames = ['Travel', 'Office Supplies', 'Software Subscriptions', 'Marketing', 'Utilities']; + + $categories = collect($categoryNames)->map(fn ($name) => ExpenseCategory::create([ + 'name' => $name, + 'company_id' => $companyId, + 'description' => "Expenses for {$name}", + ])); + + $expenseDescriptions = [ + 'Flight tickets to client meeting', + 'Hotel stay for conference', + 'Printer paper and ink cartridges', + 'Pens, notebooks, and folders', + 'Adobe Creative Cloud annual plan', + 'Slack Business subscription', + 'Google Workspace plan', + 'Facebook ads campaign', + 'LinkedIn sponsored posts', + 'Electricity bill', + 'Internet service bill', + 'Postage and shipping fees', + ]; + + collect($expenseDescriptions)->each(function (string $notes, int $index) use ($companyId, $customers, $categories) { + Expense::create([ + 'expense_date' => now()->subDays(rand(1, 90))->toDateString(), + 'expense_category_id' => $categories->random()->id, + 'expense_number' => 'EXP-'.str_pad($index + 1, 5, '0', STR_PAD_LEFT), + 'company_id' => $companyId, + 'amount' => rand(500, 100000), + 'base_amount' => rand(500, 100000), + 'notes' => $notes, + 'attachment_receipt' => null, + 'customer_id' => $customers->random()->id, + 'currency_id' => 1, + 'exchange_rate' => 1, + ]); + }); + + // ── 10. Recurring invoices ──────────────────────────────────────────── + + $recurringStatuses = ['ACTIVE', 'ON_HOLD', 'COMPLETED']; + + foreach ($recurringStatuses as $rStatus) { + $customer = $customers->random(); + $lineItems = $items->random(2); + $subTotal = $lineItems->sum('price'); + $total = (int) ($subTotal * 1.10); + + RecurringInvoice::create([ + 'starts_at' => now()->subMonths(rand(1, 6))->toDateTimeString(), + 'send_automatically' => false, + 'status' => $rStatus, + 'tax_per_item' => 'NO', + 'tax_included' => false, + 'discount_per_item' => 'NO', + 'sub_total' => $subTotal, + 'total' => $total, + 'tax' => $total - $subTotal, + 'due_amount' => $total, + 'discount' => 0, + 'discount_val' => 0, + 'company_id' => $companyId, + 'customer_id' => $customer->id, + 'frequency' => '0 0 1 * *', // monthly, 1st of month + 'limit_by' => 'NONE', + 'limit_count' => null, + 'limit_date' => null, + 'exchange_rate' => 1, + 'template_name' => 'invoice1', + ]); + } + } +}