Compare commits
3 Commits
master
...
626-introd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aba252824c | ||
|
|
84aaa4baff | ||
|
|
6e13185f84 |
@@ -15,7 +15,7 @@ DB_USERNAME=
|
||||
DB_PASSWORD=
|
||||
|
||||
SESSION_DOMAIN=null
|
||||
SANCTUM_STATEFUL_DOMAIN=
|
||||
SANCTUM_STATEFUL_DOMAINS=
|
||||
TRUSTED_PROXIES="*"
|
||||
|
||||
# Dompdf: keep false so untrusted HTML in PDF notes cannot trigger outbound requests (SSRF).
|
||||
|
||||
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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
193
docker/development/init-dev.sh
Executable file
193
docker/development/init-dev.sh
Executable file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# init-dev.sh — Reset and initialise the InvoiceShelf dev environment.
|
||||
#
|
||||
# Run this script INSIDE the PHP container:
|
||||
#
|
||||
# docker exec -it invoiceshelf-dev-php bash /var/www/html/docker/development/init-dev.sh [options]
|
||||
#
|
||||
# Safe to re-run at any time, including after switching git tags/branches.
|
||||
# Always does a full clean slate: fresh DB, cleared caches, reinstalled deps.
|
||||
#
|
||||
# Usage:
|
||||
# init-dev.sh [--db=sqlite|mysql|pgsql] [--seed] [--no-seed]
|
||||
#
|
||||
# --db=sqlite Use SQLite (default — no external DB container needed)
|
||||
# --db=mysql Use MariaDB (requires docker-compose.mysql.yml)
|
||||
# --db=pgsql Use PostgreSQL (requires docker-compose.pgsql.yml)
|
||||
# --seed Seed the database with fake dev data (DevDataSeeder)
|
||||
# --no-seed Skip seeding (default)
|
||||
#
|
||||
# Examples:
|
||||
# init-dev.sh # sqlite, no seed
|
||||
# init-dev.sh --seed # sqlite + fake data
|
||||
# init-dev.sh --db=mysql --seed # MariaDB + fake data
|
||||
# init-dev.sh --db=pgsql --no-seed # PostgreSQL, no seed
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="/var/www/html"
|
||||
cd "$ROOT"
|
||||
|
||||
# ── Argument parsing ──────────────────────────────────────────────────────────
|
||||
|
||||
DB=sqlite
|
||||
SEED=prompt
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--db=sqlite) DB=sqlite ;;
|
||||
--db=mysql) DB=mysql ;;
|
||||
--db=pgsql) DB=pgsql ;;
|
||||
--seed) SEED=true ;;
|
||||
--no-seed) SEED=false ;;
|
||||
*)
|
||||
echo "Unknown argument: $arg"
|
||||
echo "Usage: $0 [--db=sqlite|mysql|pgsql] [--seed] [--no-seed]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
step() { echo; echo "▶ $*"; }
|
||||
|
||||
# ── 1. PHP dependencies ───────────────────────────────────────────────────────
|
||||
# Note: JS dependencies (yarn) are NOT available in this PHP-FPM container.
|
||||
# Run "yarn install && yarn build" on your host, or use the static_builder
|
||||
# Docker stage which handles the frontend build automatically.
|
||||
|
||||
step "Installing PHP dependencies"
|
||||
composer install --no-interaction --prefer-dist
|
||||
|
||||
# ── 2. Environment file ───────────────────────────────────────────────────────
|
||||
|
||||
ENV_FILE="$ROOT/.env"
|
||||
|
||||
# Helper: replace or append a key=value in .env
|
||||
set_env() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
if grep -q "^${key}=" "$ENV_FILE"; then
|
||||
sed -i "s|^${key}=.*|${key}=${value}|" "$ENV_FILE"
|
||||
else
|
||||
echo "${key}=${value}" >> "$ENV_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
step "Setting up .env"
|
||||
cp "$ROOT/.env.example" "$ENV_FILE"
|
||||
php artisan key:generate --force
|
||||
|
||||
set_env "APP_ENV" "local"
|
||||
set_env "APP_DEBUG" "true"
|
||||
set_env "APP_URL" "http://invoiceshelf.test"
|
||||
|
||||
# Database — values differ per backend
|
||||
case "$DB" in
|
||||
sqlite)
|
||||
SQLITE_PATH="$ROOT/storage/app/database.sqlite"
|
||||
set_env "DB_CONNECTION" "sqlite"
|
||||
set_env "DB_DATABASE" "$SQLITE_PATH"
|
||||
;;
|
||||
mysql)
|
||||
set_env "DB_CONNECTION" "mysql"
|
||||
set_env "DB_HOST" "db"
|
||||
set_env "DB_PORT" "3306"
|
||||
set_env "DB_DATABASE" "invoiceshelf"
|
||||
set_env "DB_USERNAME" "invoiceshelf"
|
||||
set_env "DB_PASSWORD" "invoiceshelf"
|
||||
;;
|
||||
pgsql)
|
||||
set_env "DB_CONNECTION" "pgsql"
|
||||
set_env "DB_HOST" "db"
|
||||
set_env "DB_PORT" "5432"
|
||||
set_env "DB_DATABASE" "invoiceshelf"
|
||||
set_env "DB_USERNAME" "invoiceshelf"
|
||||
set_env "DB_PASSWORD" "invoiceshelf"
|
||||
;;
|
||||
esac
|
||||
|
||||
set_env "CACHE_STORE" "file"
|
||||
set_env "SESSION_DRIVER" "file"
|
||||
set_env "SESSION_LIFETIME" "120"
|
||||
set_env "SESSION_DOMAIN" "invoiceshelf.test"
|
||||
set_env "SANCTUM_STATEFUL_DOMAINS" "invoiceshelf.test"
|
||||
|
||||
# Mailpit — fake SMTP available inside the dev Docker network
|
||||
set_env "MAIL_MAILER" "smtp"
|
||||
set_env "MAIL_HOST" "mail"
|
||||
set_env "MAIL_PORT" "1025"
|
||||
set_env "MAIL_ENCRYPTION" "none"
|
||||
set_env "MAIL_USERNAME" ""
|
||||
set_env "MAIL_PASSWORD" ""
|
||||
|
||||
# ── 3. Clear ALL Laravel caches ───────────────────────────────────────────────
|
||||
|
||||
step "Clearing all Laravel caches"
|
||||
php artisan optimize:clear
|
||||
php artisan cache:clear
|
||||
php artisan clear-compiled
|
||||
|
||||
step "Clearing runtime session and view files"
|
||||
find "$ROOT/storage/framework/sessions" -type f ! -name '.gitignore' -delete 2>/dev/null || true
|
||||
find "$ROOT/storage/framework/views" -name '*.php' -delete 2>/dev/null || true
|
||||
find "$ROOT/storage/framework/cache/data" -type f ! -name '.gitignore' -delete 2>/dev/null || true
|
||||
|
||||
# ── 4. Database — full reset ──────────────────────────────────────────────────
|
||||
|
||||
if [ "$DB" = "sqlite" ]; then
|
||||
step "Resetting SQLite database"
|
||||
mkdir -p "$(dirname "$SQLITE_PATH")"
|
||||
cp "$ROOT/database/stubs/sqlite.empty.db" "$SQLITE_PATH"
|
||||
chown www-data:www-data "$SQLITE_PATH" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
step "Running all migrations from scratch (migrate:fresh)"
|
||||
php artisan migrate:fresh --force
|
||||
|
||||
step "Seeding reference data (currencies, countries)"
|
||||
php artisan db:seed --class=CurrenciesTableSeeder --force
|
||||
php artisan db:seed --class=CountriesTableSeeder --force
|
||||
|
||||
# ── 5. Optional dev data seeding ─────────────────────────────────────────────
|
||||
|
||||
if [ "$SEED" = prompt ]; then
|
||||
echo
|
||||
read -r -p "Seed the database with fake dev data? [Y/n] " reply
|
||||
case "${reply:-Y}" in
|
||||
[Yy]*|"") SEED=true ;;
|
||||
*) SEED=false ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ "$SEED" = true ]; then
|
||||
step "Seeding fake dev data"
|
||||
php artisan db:seed --class=DevDataSeeder --force
|
||||
echo
|
||||
echo " Credentials seeded:"
|
||||
echo " Admin : admin@invoiceshelf.com / password"
|
||||
echo " Staff : jane@invoiceshelf.com / password"
|
||||
echo " Staff : bob@invoiceshelf.com / password"
|
||||
else
|
||||
echo
|
||||
echo " Seeding skipped. Run again with --seed, or:"
|
||||
echo " php artisan db:seed --class=DevDataSeeder"
|
||||
fi
|
||||
|
||||
# ── 6. Storage ────────────────────────────────────────────────────────────────
|
||||
|
||||
step "Recreating public storage symlink"
|
||||
rm -f "$ROOT/public/storage"
|
||||
php artisan storage:link
|
||||
|
||||
# ── 7. Permissions ────────────────────────────────────────────────────────────
|
||||
|
||||
step "Fixing permissions"
|
||||
chmod -R 775 storage/framework storage/logs bootstrap/cache
|
||||
|
||||
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo
|
||||
echo "✓ Dev environment ready — http://invoiceshelf.test"
|
||||
echo
|
||||
Reference in New Issue
Block a user