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

14 KiB

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

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
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:

protected $fillable = [
    'supplier_quote_id', 'description', 'unit', 'quantity', 'unit_price', 'total_price', 'is_vatable',
];

protected $casts = [
    'is_vatable' => 'boolean',
];
  • Step 4: Commit
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

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:

// 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:

use App\Http\Controllers\Settings\VatSettingController;
  • Step 3: Create the VAT settings view

Create resources/views/settings/vat.blade.php:

@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
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"

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:

<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
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:

$vatRate = (float) Setting::get('vat_rate', 0);

Update the return to:

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:

$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
git add app/Http/Controllers/Purchase/RfqPortalController.php
git commit -m "feat: load VAT rate and store is_vatable in RFQ portal"

Files:

  • Modify: resources/views/rfq/show.blade.php

  • Step 1: Add VAT rate JS variable

At the start of the <script> block, add:

var _vatRate = {{ $vatRate }};
  • Step 2: Replace calcRow() and add recalcTotals()

Replace the entire calcRow function and grand total logic with:

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:

<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:

@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:

<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
git add resources/views/rfq/show.blade.php
git commit -m "feat: add VAT checkbox column and live breakdown to RFQ portal"