feat(store): complete partner store overhaul with mapping, household search, financial analytics, and resident notification system

This commit is contained in:
Developer
2026-07-02 12:47:50 +08:00
parent 647af45b99
commit c4cbdb42c7
20 changed files with 692 additions and 227 deletions

View File

@@ -347,7 +347,7 @@ class AdminPartnerStoreController extends ApiController
// For now, we'll return the data needed to render a printable QR.
return $this->ok([
'serial' => $qrCode->serial,
'qr_data' => route('qr.verify', ['serial' => $qrCode->serial]),
'qr_data' => route('api.v1.admin.qr-codes.show', ['serial' => $qrCode->serial]),
]);
}
@@ -365,6 +365,7 @@ class AdminPartnerStoreController extends ApiController
'total_commission_pesos' => number_format($totalCommission / 100, 2),
'total_settled_pesos' => number_format($totalSettled / 100, 2),
'balance_due_pesos' => number_format($balanceDue / 100, 2),
'gross_profit_pesos' => number_format(($totalRetailRevenue - $totalWholesaleCost) / 100, 2),
'total_issued_codes' => (int) $store->purchases()->sum('quantity'),
'total_sold_codes' => (int) $store->sales()->sum('quantity'),
]);

View File

@@ -4,11 +4,14 @@ namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Responses\ApiResponse;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
abstract class ApiController extends Controller
{
use AuthorizesRequests;
protected function ok(mixed $data = null, ?string $message = null, array $meta = []): JsonResponse
{
return ApiResponse::success($data, $message, $meta);

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Api\V1\Me;
use App\Http\Controllers\Api\V1\ApiController;
use App\Models\Household;
use App\Models\StoreSale;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class MySalesController extends ApiController
{
/**
* Get the household associated with the authenticated user.
*/
private function getHousehold(): ?Household
{
return Household::where('head_user_id', auth()->id())
->orWhereHas('members', function ($q) {
$q->where('user_id', auth()->id());
})
->first();
}
/**
* List sales history for the user's household.
*/
public function index(): JsonResponse
{
$household = $this->getHousehold();
if (!$household) {
return $this->ok([], 'No household associated with this account.');
}
$sales = StoreSale::where('household_id', $household->id)
->with('store')
->latest('sold_at')
->paginate(15);
return $this->ok(
$sales->map(fn($s) => [
'id' => $s->id,
'store_name' => $s->store?->business_name ?? 'Verde Partner Store',
'quantity' => $s->quantity,
'retail_price_pesos' => number_format($s->retail_price_centavos / 100, 2),
'sold_at' => $s->sold_at?->toIso8601String() ?? $s->created_at->toIso8601String(),
'receipt_url' => route('api.v1.me.sales.receipt', $s->id),
]),
null,
[
'page' => $sales->currentPage(),
'per_page' => $sales->perPage(),
'total' => $sales->total(),
]
);
}
/**
* Download a PDF receipt for a specific sale.
*/
public function downloadReceipt(StoreSale $sale): Response
{
$household = $this->getHousehold();
if (!$household || $sale->household_id !== $household->id) {
abort(403, 'Unauthorized access to this receipt.');
}
$sale->load(['store', 'household.head', 'household.barangay']);
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pdfs.receipt', [
'sale' => $sale
]);
return $pdf->download("receipt-{$sale->id}.pdf");
}
}

View File

@@ -4,10 +4,12 @@ namespace App\Http\Controllers\Api\V1\Store;
use App\Http\Controllers\Api\V1\ApiController;
use App\Models\PartnerStore;
use App\Models\StoreSale;
use App\Models\User;
use App\Services\Store\StoreOperations;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class StorePortalController extends ApiController
{
@@ -155,11 +157,26 @@ class StorePortalController extends ApiController
return $this->ok($history);
}
public function downloadReceipt(\App\Models\StoreSale $sale): Response
{
$this->authorize('view', $sale);
$sale->load(['store', 'household.head', 'household.barangay']);
$pdf = \Barryvdh\DomPDF\Facade\Pdf::loadView('pdfs.receipt', [
'sale' => $sale
]);
return $pdf->download("receipt-{$sale->id}.pdf");
}
/**
* Search for households to record a sale.
* Search for households to record a sale. Scoped to the store's LGU.
*/
public function searchHouseholds(Request $request): JsonResponse
{
$store = $this->getStore();
$request->validate([
'q' => 'required|string|min:2',
]);
@@ -167,18 +184,46 @@ class StorePortalController extends ApiController
$term = '%' . $request->string('q') . '%';
$households = \App\Models\Household::query()
->with('head')
->with(['head', 'barangay'])
->where('tenant_id', $store->tenant_id)
->where('verification_status', \App\Models\Household::VERIFICATION_APPROVED)
->where(function($q) use ($term) {
$q->where('address_line', 'like', $term)
->orWhereHas('head', function($h) use ($term) {
$h->where('first_name', 'like', $term)
->orWhere('last_name', 'like', $term);
})
->orWhereHas('members', function($m) use ($term) {
$m->where('first_name', 'like', $term)
->orWhere('last_name', 'like', $term);
});
})
->limit(10)
->limit(15)
->get();
return $this->ok($households);
return $this->ok($households->map(fn($h) => [
'id' => $h->id,
'uuid' => $h->uuid,
'head_name' => $h->head?->full_name ?? 'Unknown Head',
'address' => $h->address_line,
'barangay' => $h->barangay?->name,
'lat' => $h->coordinates?->latitude,
'lng' => $h->coordinates?->longitude,
]));
}
/**
* Get store settings and pricing.
*/
public function getSettings(): JsonResponse
{
$store = $this->getStore();
$tenant = $store->tenant;
return $this->ok([
'retail_price_centavos' => (int) ($tenant->qr_retail_price_centavos ?? 1000),
'retail_price_pesos' => ($tenant->qr_retail_price_centavos ?? 1000) / 100,
'commission_rate_percent' => (int) $store->commission_rate_percent,
]);
}
}