mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-05-25 12:44:55 +00:00
feat: add dev database seeder
This commit is contained in:
473
database/seeders/DevDataSeeder.php
Normal file
473
database/seeders/DevDataSeeder.php
Normal file
@@ -0,0 +1,473 @@
|
||||
<?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 1–3 line items
|
||||
* - 15 estimates across all statuses, each with 1–3 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user