validate([ 'status' => ['nullable', 'in:pending_kyc,active,suspended'], 'q' => ['nullable', 'string', 'max:100'], 'per_page' => ['nullable', 'integer', 'min:1', 'max:500'], ]); $perPage = (int) $request->input('per_page', 25); $stores = PartnerStore::query() ->with(['owner', 'barangay', 'inventory']) ->when($request->filled('status'), fn ($q) => $q->where('status', $request->string('status'))) ->when($request->filled('q'), fn ($q) => $q->where('business_name', 'like', '%'.$request->string('q').'%')) ->orderBy('business_name') ->paginate($perPage); return $this->ok( PartnerStoreResource::collection($stores), null, [ 'page' => $stores->currentPage(), 'per_page' => $stores->perPage(), 'total' => $stores->total(), 'last_page' => $stores->lastPage(), ], ); } public function store(Request $request): JsonResponse { $data = $request->validate([ 'owner_user_id' => ['required', 'integer', 'exists:users,id'], 'business_name' => ['required', 'string', 'max:191'], 'business_permit_number' => ['nullable', 'string', 'max:64'], 'address_line' => ['nullable', 'string', 'max:255'], 'barangay_id' => ['nullable', 'integer', 'exists:barangays,id'], 'lat' => ['nullable', 'numeric', 'between:-90,90'], 'lng' => ['nullable', 'numeric', 'between:-180,180'], 'commission_rate_percent' => ['nullable', 'integer', 'min:0', 'max:50'], 'status' => ['nullable', Rule::in(['pending_kyc', 'active', 'suspended'])], ]); $payload = collect($data)->except(['lat', 'lng'])->all(); if (isset($data['lat'], $data['lng'])) { $payload['coordinates'] = new Point((float) $data['lat'], (float) $data['lng'], 4326); } $store = PartnerStore::create($payload); return $this->created( new PartnerStoreResource($store->load(['owner', 'barangay'])), 'Store created', ); } public function show(PartnerStore $store): JsonResponse { $store->load(['owner', 'barangay', 'inventory']); return $this->ok(new PartnerStoreResource($store)); } public function update(Request $request, PartnerStore $store): JsonResponse { $data = $request->validate([ 'business_name' => ['sometimes', 'required', 'string', 'max:191'], 'business_permit_number' => ['sometimes', 'nullable', 'string', 'max:64'], 'commission_rate_percent' => ['sometimes', 'integer', 'min:0', 'max:50'], 'status' => ['sometimes', Rule::in(['pending_kyc', 'active', 'suspended'])], 'address_line' => ['sometimes', 'nullable', 'string', 'max:255'], 'barangay_id' => ['sometimes', 'nullable', 'integer', 'exists:barangays,id'], 'lat' => ['sometimes', 'nullable', 'numeric', 'between:-90,90'], 'lng' => ['sometimes', 'nullable', 'numeric', 'between:-180,180'], ]); $payload = collect($data)->except(['lat', 'lng'])->all(); if (isset($data['lat'], $data['lng'])) { $payload['coordinates'] = new Point((float) $data['lat'], (float) $data['lng'], 4326); } $store->update($payload); return $this->ok(new PartnerStoreResource($store->fresh()->load(['owner', 'barangay', 'inventory'])), 'Store updated'); } public function issueInventory(Request $request, PartnerStore $store): JsonResponse { $data = $request->validate([ 'quantity' => ['required', 'integer', 'min:1', 'max:50000'], 'wholesale_price_centavos' => ['required', 'integer', 'min:0'], ]); if ($store->status !== PartnerStore::STATUS_ACTIVE) { return $this->fail('Store must be active to receive inventory', null, 422); } $purchase = $this->ops->issueWholesale( $store, (int) $data['quantity'], (int) $data['wholesale_price_centavos'], ); return $this->created([ 'purchase_id' => $purchase->id, 'batch_number' => $purchase->batch->batch_number, 'quantity' => $purchase->quantity, 'inventory_balance' => $store->fresh()->load('inventory')->inventory?->current_code_balance ?? 0, ], 'Inventory issued'); } public function recordSale(Request $request, PartnerStore $store): JsonResponse { $globalPrice = config('qr.default_retail_price_per_code_centavos', 1000); $data = $request->validate([ 'household_id' => ['required', 'integer', 'exists:households,id'], 'quantity' => ['required', 'integer', 'min:1', 'max:1000'], 'retail_price_per_code_centavos' => ['nullable', 'integer', 'in:' . $globalPrice], ]); $retailPrice = (int) ($data['retail_price_per_code_centavos'] ?? $globalPrice); try { $household = Household::findOrFail($data['household_id']); $sale = $this->ops->sellToHousehold( $store, $household, (int) $data['quantity'], $retailPrice, ); } catch (\DomainException $e) { return $this->fail($e->getMessage(), null, 422); } return $this->created([ 'sale_id' => $sale->id, 'quantity' => $sale->quantity, 'retail_price_centavos' => $sale->retail_price_centavos, 'commission_centavos' => $sale->commission_centavos, ], 'Sale recorded'); } public function purchases(Request $request, PartnerStore $store): JsonResponse { $purchases = $store->purchases() ->with('batch') ->latest() ->paginate(15); return $this->ok( $purchases->map(fn ($p) => [ 'id' => $p->id, 'batch_number' => $p->batch?->batch_number ?? '—', 'quantity' => $p->quantity, 'wholesale_price_pesos' => number_format($p->wholesale_price_centavos / 100, 2), 'paid_at' => $p->paid_at?->toIso8601String() ?? $p->created_at?->toIso8601String(), ]), null, [ 'page' => $purchases->currentPage(), 'per_page' => $purchases->perPage(), 'total' => $purchases->total(), 'last_page' => $purchases->lastPage(), ] ); } public function salesHistory(Request $request, PartnerStore $store): JsonResponse { $sales = $store->sales() ->with(['household.head']) ->latest() ->paginate(15); return $this->ok( $sales->map(fn ($s) => [ 'id' => $s->id, 'household_name' => $s->household?->head?->full_name ?? '—', 'household_email' => $s->household?->head?->email ?? '—', 'quantity' => $s->quantity, 'retail_price_pesos' => number_format($s->retail_price_centavos / 100, 2), 'commission_pesos' => number_format($s->commission_centavos / 100, 2), 'sold_at' => $s->sold_at?->toIso8601String() ?? $s->created_at?->toIso8601String(), ]), null, [ 'page' => $sales->currentPage(), 'per_page' => $sales->perPage(), 'total' => $sales->total(), 'last_page' => $sales->lastPage(), ] ); } public function analytics(PartnerStore $store): JsonResponse { $totalWholesaleCost = (int) $store->purchases()->sum('wholesale_price_centavos'); $totalRetailRevenue = (int) $store->sales()->sum('retail_price_centavos'); $totalCommission = (int) $store->sales()->sum('commission_centavos'); $grossProfit = $totalRetailRevenue - $totalWholesaleCost; return $this->ok([ 'total_wholesale_cost_pesos' => number_format($totalWholesaleCost / 100, 2), 'total_retail_revenue_pesos' => number_format($totalRetailRevenue / 100, 2), 'total_commission_pesos' => number_format($totalCommission / 100, 2), 'gross_profit_pesos' => number_format($grossProfit / 100, 2), 'total_issued_codes' => (int) $store->purchases()->sum('quantity'), 'total_sold_codes' => (int) $store->sales()->sum('quantity'), ]); } }