MiknasTrading/docs/superpowers/plans/2026-06-01-vat-rfq.md

441 lines
14 KiB
Markdown

# VAT Settings + RFQ Per-Item Vatable Checkbox — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a global VAT rate setting and per-item vatable checkboxes on the supplier RFQ portal, with live VAT breakdown in totals.
**Architecture:** Global VAT rate stored in existing `settings` key/value table. Per-item `is_vatable` flag added to `supplier_quote_items`. RFQ portal view updated with checkbox column and three-row footer (subtotal / VAT / grand total). New `VatSettingController` + `settings/vat` page added under the Admin-only System sidebar section.
**Tech Stack:** Laravel 12, PHP 8.2, SQLite, Blade, Alpine-free vanilla JS, Tailwind (inline styles per project convention)
---
### Task 1: Migration — add `is_vatable` to `supplier_quote_items`
**Files:**
- Modify: `database/migrations/2026_06_01_090734_add_is_vatable_to_supplier_quote_items.php`
- Modify: `app/Models/SupplierQuoteItem.php`
- [ ] **Step 1: Write the migration**
Replace the generated migration body with:
```php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('supplier_quote_items', function (Blueprint $table) {
$table->boolean('is_vatable')->default(false)->after('total_price');
});
}
public function down(): void
{
Schema::table('supplier_quote_items', function (Blueprint $table) {
$table->dropColumn('is_vatable');
});
}
};
```
- [ ] **Step 2: Run migration**
```bash
php artisan migrate
```
Expected: `Migrating: 2026_06_01_090734_add_is_vatable_to_supplier_quote_items` then `Migrated`.
- [ ] **Step 3: Update SupplierQuoteItem model**
In `app/Models/SupplierQuoteItem.php`, update `$fillable`:
```php
protected $fillable = [
'supplier_quote_id', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'is_vatable',
];
protected $casts = [
'is_vatable' => 'boolean',
];
```
- [ ] **Step 4: Commit**
```bash
git add database/migrations/2026_06_01_090734_add_is_vatable_to_supplier_quote_items.php app/Models/SupplierQuoteItem.php
git commit -m "feat: add is_vatable column to supplier_quote_items"
```
---
### Task 2: VatSettingController + route + view
**Files:**
- Create: `app/Http/Controllers/Settings/VatSettingController.php`
- Create: `resources/views/settings/vat.blade.php`
- Modify: `routes/web.php`
- [ ] **Step 1: Create VatSettingController**
Create `app/Http/Controllers/Settings/VatSettingController.php`:
```php
<?php
namespace App\Http\Controllers\Settings;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use Illuminate\Http\Request;
class VatSettingController extends Controller
{
public function index()
{
$vatRate = Setting::get('vat_rate', '0');
return view('settings.vat', compact('vatRate'));
}
public function update(Request $request)
{
$validated = $request->validate([
'vat_rate' => ['required', 'numeric', 'min:0', 'max:100'],
]);
Setting::set('vat_rate', (string) $validated['vat_rate']);
return response()->json(['message' => 'VAT rate saved.', 'vat_rate' => $validated['vat_rate']]);
}
}
```
- [ ] **Step 2: Add routes**
In `routes/web.php`, after the integrations routes (around line 148), add:
```php
// VAT settings
Route::get('settings/vat', [VatSettingController::class, 'index'])->name('settings.vat');
Route::post('settings/vat', [VatSettingController::class, 'update'])->name('settings.vat.update');
```
Also add the import at the top of the file with the other Settings controllers:
```php
use App\Http\Controllers\Settings\VatSettingController;
```
- [ ] **Step 3: Create the VAT settings view**
Create `resources/views/settings/vat.blade.php`:
```blade
@extends('layouts.app')
@section('title', 'Settings — VAT')
@section('content')
<div class="mb-5">
<h1 class="page-title">VAT Settings</h1>
<p class="page-subtitle">Set the global VAT rate applied to vatable items on supplier quotes.</p>
</div>
<div style="max-width:480px;">
<div style="background:white;border:1px solid #e2e8f0;border-radius:0.875rem;overflow:hidden;">
<div style="padding:1.25rem 1.5rem;border-bottom:1px solid #e2e8f0;background:#f8fafc;">
<div style="font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em;">VAT Configuration</div>
</div>
<div style="padding:1.5rem;">
<label style="display:block;font-size:12px;font-weight:600;color:#374151;margin-bottom:6px;">
VAT Rate (%)
</label>
<div style="display:flex;align-items:center;gap:10px;">
<input type="number" id="vat-rate-input" value="{{ $vatRate }}"
min="0" max="100" step="0.01" placeholder="e.g. 10"
style="width:160px;padding:9px 12px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:14px;font-weight:600;outline:none;"
onfocus="this.style.borderColor='#2563eb'" onblur="this.style.borderColor='#e2e8f0'">
<span style="font-size:14px;color:#64748b;font-weight:500;">%</span>
</div>
<p style="font-size:12px;color:#94a3b8;margin-top:8px;">
Enter 0 to disable VAT. Suppliers will see the VAT checkbox on their quote form when this is greater than 0.
</p>
<button onclick="saveVat()"
style="margin-top:20px;padding:10px 24px;background:#2563eb;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;">
Save VAT Rate
</button>
</div>
</div>
</div>
<script>
var CSRF = document.querySelector('meta[name="csrf-token"]').content;
function saveVat() {
var rate = document.getElementById('vat-rate-input').value;
fetch('{{ route('settings.vat.update') }}', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json', 'Content-Type': 'application/json' },
body: JSON.stringify({ vat_rate: rate })
}).then(function(r) {
return r.json().then(function(body) {
if (!r.ok) return Promise.reject(body);
return body;
});
}).then(function() {
showToast('VAT rate saved.', 'success');
}).catch(function(err) {
showToast((err.errors && err.errors.vat_rate ? err.errors.vat_rate[0] : null) || 'Failed to save.', 'error');
});
}
</script>
@endsection
```
- [ ] **Step 4: Commit**
```bash
git add app/Http/Controllers/Settings/VatSettingController.php resources/views/settings/vat.blade.php routes/web.php
git commit -m "feat: add VAT settings page and controller"
```
---
### Task 3: Add VAT link to sidebar
**Files:**
- Modify: `resources/views/layouts/app.blade.php`
- [ ] **Step 1: Add sidebar link**
In `resources/views/layouts/app.blade.php`, after the Integrations `<a>` tag (around line 198, just before `@endrole`), add:
```blade
<a href="{{ route('settings.vat') }}" style="
display:flex; align-items:center; gap:8px;
padding:7px 12px 7px 24px; border-radius:7px; margin-bottom:1px;
font-size:13px; text-decoration:none;
{{ request()->routeIs('settings.vat*') ? 'background:#1e293b;color:#fff;font-weight:500;' : 'color:#94a3b8;' }}
" onmouseover="if(!this.style.color.includes('fff'))this.style.color='#e2e8f0'" onmouseout="if(!this.style.background.includes('1e293b'))this.style.color='#94a3b8'">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="flex-shrink:0;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2z"/>
</svg>
VAT Settings
</a>
```
- [ ] **Step 2: Commit**
```bash
git add resources/views/layouts/app.blade.php
git commit -m "feat: add VAT Settings link to sidebar"
```
---
### Task 4: Update RfqPortalController — load VAT rate and store is_vatable
**Files:**
- Modify: `app/Http/Controllers/Purchase/RfqPortalController.php`
- [ ] **Step 1: Update show() to pass VAT rate**
Add `use App\Models\Setting;` at the top of the file.
Update the `show()` method — add before the `return view(...)` line:
```php
$vatRate = (float) Setting::get('vat_rate', 0);
```
Update the return to:
```php
return view('rfq.show', compact('invitation', 'purchaseRequest', 'items', 'confirmCode', 'vatRate'));
```
- [ ] **Step 2: Update submit() to store is_vatable and calculate VAT-inclusive total**
Add `'items.*.is_vatable' => ['nullable', 'boolean']` to the validation rules array.
Replace the items loop and total calculation:
```php
$subtotal = 0;
$vatAmount = 0;
$vatRate = (float) Setting::get('vat_rate', 0);
foreach ($purchaseItems as $i => $item) {
$unitPrice = (float)($validated['items'][$i]['unit_price'] ?? 0);
$qty = (float)$item->quantity_required;
$totalPrice = round($unitPrice * $qty, 3);
$isVatable = !empty($validated['items'][$i]['is_vatable']);
$subtotal += $totalPrice;
if ($isVatable && $vatRate > 0) {
$vatAmount += round($totalPrice * $vatRate / 100, 3);
}
SupplierQuoteItem::create([
'supplier_quote_id' => $quote->id,
'description' => $item->description,
'unit' => $item->unit ?? '',
'quantity' => $qty,
'unit_price' => $unitPrice,
'total_price' => $totalPrice,
'is_vatable' => $isVatable,
]);
}
$grand = round($subtotal + $vatAmount, 3);
$quote->update(['total_amount' => $grand]);
```
- [ ] **Step 3: Commit**
```bash
git add app/Http/Controllers/Purchase/RfqPortalController.php
git commit -m "feat: load VAT rate and store is_vatable in RFQ portal"
```
---
### Task 5: Update rfq/show.blade.php — checkbox column + VAT footer + JS
**Files:**
- Modify: `resources/views/rfq/show.blade.php`
- [ ] **Step 1: Add VAT rate JS variable**
At the start of the `<script>` block, add:
```javascript
var _vatRate = {{ $vatRate }};
```
- [ ] **Step 2: Replace calcRow() and add recalcTotals()**
Replace the entire `calcRow` function and grand total logic with:
```javascript
var _totals = {};
var _vatable = {};
function calcRow(i, qty) {
var inp = document.querySelector('input[name="items[' + i + '][unit_price]"]');
var cb = document.querySelector('input[name="items[' + i + '][is_vatable]"]');
var up = parseFloat(inp ? inp.value : 0) || 0;
var tot = Math.round(up * qty * 1000) / 1000;
_totals[i] = tot;
_vatable[i] = cb ? cb.checked : false;
var el = document.getElementById('tot-' + i);
if (el) el.textContent = tot > 0 ? 'BD ' + tot.toFixed(3) : '—';
recalcTotals();
}
function recalcTotals() {
var subtotal = 0;
var vatAmount = 0;
Object.keys(_totals).forEach(function(i) {
subtotal += _totals[i];
if (_vatable[i] && _vatRate > 0) {
vatAmount += Math.round(_totals[i] * _vatRate / 100 * 1000) / 1000;
}
});
var grand = Math.round((subtotal + vatAmount) * 1000) / 1000;
var elSub = document.getElementById('subtotal-row');
var elVat = document.getElementById('vat-row');
var elGrand = document.getElementById('grand-total');
if (elSub) elSub.textContent = 'BD ' + subtotal.toFixed(3);
if (elVat) {
elVat.textContent = 'BD ' + vatAmount.toFixed(3);
var vatTr = document.getElementById('vat-tr');
if (vatTr) vatTr.style.display = _vatRate > 0 ? '' : 'none';
}
if (elGrand) elGrand.textContent = 'BD ' + grand.toFixed(3);
}
```
- [ ] **Step 3: Update table header**
Replace the `<thead>` block:
```html
<thead>
<tr>
<th>#</th>
<th>Description</th>
<th>Qty</th>
<th>Unit</th>
<th style="text-align:center;">VAT?</th>
<th style="text-align:right;">Unit Price (BD)</th>
<th style="text-align:right;">Total (BD)</th>
</tr>
</thead>
```
- [ ] **Step 4: Update each item row**
Replace the `@foreach` tbody rows:
```blade
@foreach($items as $i => $item)
<tr>
<td style="color:#94a3b8;font-size:12px;">{{ $i + 1 }}</td>
<td style="font-weight:500;">{{ $item->description }}</td>
<td>{{ rtrim(rtrim(number_format((float)$item->quantity_required, 3), '0'), '.') }}</td>
<td style="color:#64748b;">{{ $item->unit ?: '—' }}</td>
<td style="text-align:center;">
@if($vatRate > 0)
<input type="checkbox" name="items[{{ $i }}][is_vatable]" value="1"
onchange="calcRow({{ $i }}, {{ (float)$item->quantity_required }})"
style="width:16px;height:16px;accent-color:#2563eb;cursor:pointer;">
@else
<span style="color:#cbd5e1;font-size:11px;">—</span>
@endif
</td>
<td style="text-align:right;">
<input type="number" class="price" name="items[{{ $i }}][unit_price]"
min="0" step="0.001" required placeholder="0.000"
oninput="calcRow({{ $i }}, {{ (float)$item->quantity_required }})">
</td>
<td style="text-align:right;font-weight:600;" id="tot-{{ $i }}">—</td>
</tr>
@endforeach
```
- [ ] **Step 5: Update tfoot**
Replace the `<tfoot>` block:
```html
<tfoot>
<tr style="background:#f8fafc;">
<td colspan="6" style="text-align:right;font-size:13px;color:#475569;">Subtotal:</td>
<td style="text-align:right;font-size:14px;color:#475569;" id="subtotal-row">BD 0.000</td>
</tr>
<tr id="vat-tr" style="background:#fffbeb;{{ $vatRate > 0 ? '' : 'display:none;' }}">
<td colspan="6" style="text-align:right;font-size:13px;color:#92400e;">VAT ({{ $vatRate }}%):</td>
<td style="text-align:right;font-size:14px;color:#92400e;" id="vat-row">BD 0.000</td>
</tr>
<tr style="background:#f8fafc;border-top:2px solid #e2e8f0;">
<td colspan="6" style="text-align:right;font-size:13px;color:#475569;font-weight:700;">Grand Total:</td>
<td style="text-align:right;font-size:15px;color:#2563eb;font-weight:700;" id="grand-total">BD 0.000</td>
</tr>
</tfoot>
```
- [ ] **Step 6: Commit**
```bash
git add resources/views/rfq/show.blade.php
git commit -m "feat: add VAT checkbox column and live breakdown to RFQ portal"
```