Files
InvoiceShelf/database/seeders/DevDataSeeder.php
2026-04-13 22:56:34 +00:00

474 lines
19 KiB
PHP
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.
<?php
namespace Database\Seeders;
use App\Facades\Hashids;
use App\Models\Company;
use App\Models\CompanySetting;
use App\Models\Customer;
use App\Models\Estimate;
use App\Models\EstimateItem;
use App\Models\Expense;
use App\Models\ExpenseCategory;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Item;
use App\Models\Payment;
use App\Models\RecurringInvoice;
use App\Models\Setting;
use App\Models\TaxType;
use App\Models\Unit;
use App\Models\User;
use App\Services\SerialNumberFormatter;
use App\Space\InstallUtils;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Silber\Bouncer\BouncerFacade;
/**
* Seeds the database with realistic fake data for development and manual testing.
*
* Usage:
* php artisan db:seed --class=DevDataSeeder
*
* This creates:
* - An admin user (admin@invoiceshelf.com / password: password)
* - Two additional staff users
* - One company with all default data (payment methods, units, roles)
* - Tax types (VAT, GST)
* - 15 catalogue items
* - 10 customers
* - 20 invoices across all statuses, each with 13 line items
* - 15 estimates across all statuses, each with 13 line items
* - 10 payments linked to paid/partially-paid invoices
* - 12 expenses across several categories
* - 3 recurring invoices
*/
class DevDataSeeder extends Seeder
{
public function run(): void
{
// ── 1. Admin user & company ───────────────────────────────────────────
$admin = User::create([
'name' => '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',
]);
}
}
}