mdn-backend / app /Domain /Essentials /EssentialsService.php
internationalscholarsprogram's picture
feat(api): deploy connect and essentials endpoints to HF space
3bdbcea
Raw
History Blame Contribute Delete
18.8 kB
<?php
namespace App\Domain\Essentials;
use App\Infrastructure\Ids\UuidGenerator;
use Illuminate\Support\Facades\DB;
class EssentialsService
{
private const RESOURCE_TABLES = [
'categories' => 'essentials_categories',
'vendors' => 'essentials_vendors',
'listings' => 'essentials_listings',
'requests' => 'essentials_requests',
'reviews' => 'essentials_reviews',
'complaints' => 'essentials_complaints',
'promotions' => 'essentials_promotions',
];
public function __construct(private UuidGenerator $ids)
{
}
public function dashboard(): array
{
$counts = [];
foreach (self::RESOURCE_TABLES as $resource => $table) {
$query = DB::table($table);
if ($this->hasSoftDeletes($resource)) {
$query->whereNull('deleted_at');
}
$counts[$resource] = $query->count();
}
return [
'metrics' => [
'activeCategories' => DB::table('essentials_categories')->where('is_active', true)->whereNull('deleted_at')->count(),
'approvedVendors' => DB::table('essentials_vendors')->where('status', 'approved')->whereNull('deleted_at')->count(),
'publishedListings' => DB::table('essentials_listings')->where('status', 'published')->whereNull('deleted_at')->count(),
'openRequests' => DB::table('essentials_requests')->whereNotIn('status', ['closed', 'cancelled'])->whereNull('deleted_at')->count(),
'openComplaints' => DB::table('essentials_complaints')->whereIn('status', ['open', 'investigating'])->count(),
'activePromotions' => DB::table('essentials_promotions')->where('is_active', true)->count(),
],
'counts' => $counts,
'recentRequests' => $this->list('requests', ['per_page' => 5])['items'],
'featuredListings' => $this->list('listings', ['per_page' => 5, 'status' => 'published'])['items'],
];
}
public function list(string $resource, array $filters = []): array
{
$table = $this->tableFor($resource);
$page = max(1, (int) ($filters['page'] ?? 1));
$perPage = min(100, max(1, (int) ($filters['per_page'] ?? 25)));
$search = trim((string) ($filters['search'] ?? ''));
$status = trim((string) ($filters['status'] ?? ''));
$query = DB::table($table);
if ($this->hasSoftDeletes($resource)) {
$query->whereNull('deleted_at');
}
if ($status !== '' && $status !== 'all') {
if ($resource === 'categories') {
$query->where('is_active', in_array($status, ['active', '1', 'true'], true));
} elseif ($resource === 'promotions') {
$query->where('is_active', in_array($status, ['active', '1', 'true'], true));
} else {
$query->where('status', $status);
}
}
if ($search !== '') {
$this->applySearch($query, $resource, $search);
}
$total = (clone $query)->count();
$rows = $query
->orderByDesc($this->orderColumn($resource))
->offset(($page - 1) * $perPage)
->limit($perPage)
->get();
return [
'items' => $rows->map(fn (object $row) => $this->recordPayload($resource, $row))->all(),
'total' => $total,
'page' => $page,
'per_page' => $perPage,
];
}
public function get(string $resource, string $id): ?array
{
$table = $this->tableFor($resource);
$query = DB::table($table)->where('id', $id);
if ($this->hasSoftDeletes($resource)) {
$query->whereNull('deleted_at');
}
$row = $query->first();
return $row === null ? null : $this->recordPayload($resource, $row);
}
public function save(string $resource, array $input): array
{
$table = $this->tableFor($resource);
$id = (string) ($input['id'] ?? $input[$this->idKey($resource)] ?? $this->ids->generate());
$now = now();
$payload = $this->writePayload($resource, $input, $id, $now);
$exists = DB::table($table)->where('id', $id)->exists();
if ($exists) {
$updates = $payload;
if ($resource !== 'reviews') {
$updates['updated_at'] = $now;
}
DB::table($table)->where('id', $id)->update($updates);
} else {
$insert = array_merge($payload, ['created_at' => $now]);
if ($resource !== 'reviews') {
$insert['updated_at'] = $now;
}
DB::table($table)->insert($insert);
}
return $this->get($resource, $id) ?? [];
}
public function transition(string $resource, string $id, string $status, ?string $note = null, ?string $actorId = null): array
{
$table = $this->tableFor($resource);
$existing = DB::table($table)->where('id', $id)->first();
if ($existing === null) {
return [];
}
if ($resource === 'categories') {
DB::table($table)->where('id', $id)->update(['is_active' => $status === 'active', 'updated_at' => now()]);
} elseif ($resource === 'promotions') {
DB::table($table)->where('id', $id)->update(['is_active' => $status === 'active', 'updated_at' => now()]);
} else {
$updates = ['status' => $status];
if ($resource !== 'reviews') {
$updates['updated_at'] = now();
}
DB::table($table)->where('id', $id)->update($updates);
}
if ($resource === 'requests') {
DB::table('essentials_request_status_history')->insert([
'id' => $this->ids->generate(),
'request_id' => $id,
'from_status' => $existing->status ?? null,
'to_status' => $status,
'changed_by' => $actorId,
'note' => $note,
'created_at' => now(),
]);
}
return $this->get($resource, $id) ?? [];
}
private function writePayload(string $resource, array $input, string $id, mixed $now): array
{
return match ($resource) {
'categories' => [
'id' => $id,
'parent_id' => $input['parent_id'] ?? $input['parentId'] ?? null,
'name' => (string) ($input['name'] ?? $input['title'] ?? 'Essentials category'),
'slug' => (string) ($input['slug'] ?? $this->slug($input['name'] ?? $input['title'] ?? $id)),
'description' => $input['description'] ?? null,
'icon' => $input['icon'] ?? null,
'sort_order' => (int) ($input['sort_order'] ?? $input['sortOrder'] ?? 0),
'is_active' => (bool) ($input['is_active'] ?? $input['isActive'] ?? true),
'row_version' => (int) ($input['row_version'] ?? 1),
],
'vendors' => [
'id' => $id,
'vendor_number' => $input['vendor_number'] ?? $input['vendorNumber'] ?? null,
'vendor_type' => $input['vendor_type'] ?? $input['vendorType'] ?? 'general_marketplace_seller',
'category_id' => $input['category_id'] ?? $input['categoryId'] ?? null,
'business_name' => (string) ($input['business_name'] ?? $input['businessName'] ?? $input['title'] ?? 'Essentials vendor'),
'country' => $input['country'] ?? null,
'city' => $input['city'] ?? null,
'business_email' => $input['business_email'] ?? $input['businessEmail'] ?? null,
'business_phone' => $input['business_phone'] ?? $input['businessPhone'] ?? null,
'business_description' => $input['business_description'] ?? $input['description'] ?? null,
'services_offered' => $input['services_offered'] ?? $input['servicesOffered'] ?? null,
'status' => $input['status'] ?? 'draft',
'risk_level' => $input['risk_level'] ?? $input['riskLevel'] ?? null,
'row_version' => (int) ($input['row_version'] ?? 1),
],
'listings' => [
'id' => $id,
'category_id' => (string) ($input['category_id'] ?? $input['categoryId'] ?? $this->defaultCategoryId()),
'vendor_id' => $input['vendor_id'] ?? $input['vendorId'] ?? null,
'title' => (string) ($input['title'] ?? 'Essentials listing'),
'slug' => (string) ($input['slug'] ?? $this->slug($input['title'] ?? $id)),
'description' => $input['description'] ?? null,
'listing_type' => $input['listing_type'] ?? $input['listingType'] ?? 'fixed_price',
'price_amount' => $input['price_amount'] ?? $input['priceAmount'] ?? null,
'price_max' => $input['price_max'] ?? $input['priceMax'] ?? null,
'currency' => $input['currency'] ?? 'KES',
'member_offer_text' => $input['member_offer_text'] ?? $input['memberOfferText'] ?? null,
'delivery_timeline' => $input['delivery_timeline'] ?? $input['deliveryTimeline'] ?? null,
'location_served' => $input['location_served'] ?? $input['locationServed'] ?? null,
'status' => $input['status'] ?? 'draft',
'published_at' => ($input['status'] ?? null) === 'published' ? $now : null,
'row_version' => (int) ($input['row_version'] ?? 1),
],
'requests' => [
'id' => $id,
'reference_no' => $input['reference_no'] ?? $input['referenceNo'] ?? 'ESS-'.strtoupper(substr($id, -6)),
'member_id' => (string) ($input['member_id'] ?? $input['memberId'] ?? '00000000-0000-7000-8000-000000000101'),
'listing_id' => $input['listing_id'] ?? $input['listingId'] ?? null,
'category_id' => (string) ($input['category_id'] ?? $input['categoryId'] ?? $this->defaultCategoryId()),
'request_type' => $input['request_type'] ?? $input['requestType'] ?? 'custom_request',
'title' => (string) ($input['title'] ?? 'Essentials request'),
'status' => $input['status'] ?? 'draft',
'budget_amount' => $input['budget_amount'] ?? $input['budgetAmount'] ?? null,
'currency' => $input['currency'] ?? 'KES',
'timeline' => $input['timeline'] ?? null,
'location' => $input['location'] ?? null,
'contact_details' => $this->jsonOrNull($input['contact_details'] ?? $input['contactDetails'] ?? null),
'beneficiary_details' => $this->jsonOrNull($input['beneficiary_details'] ?? $input['beneficiaryDetails'] ?? null),
'form_payload' => $this->jsonOrNull($input['form_payload'] ?? $input['formPayload'] ?? null),
'priority' => $input['priority'] ?? 'normal',
'assigned_admin_id' => $input['assigned_admin_id'] ?? $input['assignedAdminId'] ?? null,
'submitted_at' => in_array(($input['status'] ?? ''), ['submitted', 'in_progress'], true) ? $now : null,
'row_version' => (int) ($input['row_version'] ?? 1),
],
'complaints' => [
'id' => $id,
'request_id' => $input['request_id'] ?? $input['requestId'] ?? null,
'review_id' => $input['review_id'] ?? $input['reviewId'] ?? null,
'member_id' => (string) ($input['member_id'] ?? $input['memberId'] ?? '00000000-0000-7000-8000-000000000101'),
'vendor_id' => $input['vendor_id'] ?? $input['vendorId'] ?? null,
'subject' => (string) ($input['subject'] ?? $input['title'] ?? 'Essentials complaint'),
'description' => (string) ($input['description'] ?? ''),
'severity' => $input['severity'] ?? 'medium',
'status' => $input['status'] ?? 'open',
'resolution_note' => $input['resolution_note'] ?? $input['resolutionNote'] ?? null,
'row_version' => (int) ($input['row_version'] ?? 1),
],
'reviews' => [
'id' => $id,
'request_id' => (string) ($input['request_id'] ?? $input['requestId'] ?? '00000000-0000-7000-8000-000000003004'),
'listing_id' => $input['listing_id'] ?? $input['listingId'] ?? null,
'vendor_id' => $input['vendor_id'] ?? $input['vendorId'] ?? null,
'member_id' => (string) ($input['member_id'] ?? $input['memberId'] ?? '00000000-0000-7000-8000-000000000101'),
'service_quality' => (int) ($input['service_quality'] ?? $input['serviceQuality'] ?? 5),
'responsiveness' => (int) ($input['responsiveness'] ?? 5),
'reliability' => (int) ($input['reliability'] ?? 5),
'value_for_money' => (int) ($input['value_for_money'] ?? $input['valueForMoney'] ?? 5),
'overall_satisfaction' => (int) ($input['overall_satisfaction'] ?? $input['overallSatisfaction'] ?? 5),
'would_recommend' => (bool) ($input['would_recommend'] ?? $input['wouldRecommend'] ?? true),
'written_feedback' => $input['written_feedback'] ?? $input['writtenFeedback'] ?? null,
'complaint_flag' => (bool) ($input['complaint_flag'] ?? $input['complaintFlag'] ?? false),
'visibility' => $input['visibility'] ?? 'internal',
'status' => $input['status'] ?? 'pending',
],
'promotions' => [
'id' => $id,
'listing_id' => $input['listing_id'] ?? $input['listingId'] ?? null,
'vendor_id' => $input['vendor_id'] ?? $input['vendorId'] ?? null,
'category_id' => $input['category_id'] ?? $input['categoryId'] ?? null,
'title' => (string) ($input['title'] ?? 'Essentials promotion'),
'offer_type' => $input['offer_type'] ?? $input['offerType'] ?? 'discount',
'value' => $input['value'] ?? null,
'description' => $input['description'] ?? null,
'starts_at' => $input['starts_at'] ?? $input['startsAt'] ?? null,
'ends_at' => $input['ends_at'] ?? $input['endsAt'] ?? null,
'is_active' => (bool) ($input['is_active'] ?? $input['isActive'] ?? true),
'row_version' => (int) ($input['row_version'] ?? 1),
],
default => ['id' => $id],
};
}
private function recordPayload(string $resource, object $row): array
{
$raw = (array) $row;
$title = match ($resource) {
'categories' => $row->name,
'vendors' => $row->business_name,
'listings' => $row->title,
'requests' => $row->title,
'reviews' => 'Review '.$row->id,
'complaints' => $row->subject,
'promotions' => $row->title,
default => $row->id,
};
$status = match ($resource) {
'categories' => ((int) $row->is_active) === 1 ? 'active' : 'inactive',
'promotions' => ((int) $row->is_active) === 1 ? 'active' : 'inactive',
default => (string) ($row->status ?? 'active'),
};
$summary = match ($resource) {
'categories' => (string) ($row->description ?? ''),
'vendors' => (string) ($row->services_offered ?? $row->business_description ?? ''),
'listings' => trim((string) ($row->description ?? '').' '.($row->price_amount ? $row->currency.' '.$row->price_amount : '')),
'requests' => trim(($row->reference_no ?? '').' '.($row->budget_amount ? $row->currency.' '.$row->budget_amount : '')),
'reviews' => (string) ($row->written_feedback ?? 'Overall satisfaction '.$row->overall_satisfaction),
'complaints' => (string) ($row->description ?? ''),
'promotions' => (string) ($row->description ?? $row->offer_type),
default => '',
};
return [
'id' => $row->id,
'title' => $title,
'status' => $status,
'summary' => $summary,
'updatedAt' => (string) ($row->updated_at ?? $row->created_at ?? ''),
'resource' => $resource,
'raw' => $raw,
] + $raw;
}
private function applySearch(mixed $query, string $resource, string $search): void
{
$columns = match ($resource) {
'categories' => ['name', 'slug', 'description'],
'vendors' => ['business_name', 'vendor_number', 'business_email', 'services_offered'],
'listings' => ['title', 'slug', 'description', 'location_served'],
'requests' => ['reference_no', 'title', 'location', 'timeline'],
'reviews' => ['written_feedback'],
'complaints' => ['subject', 'description', 'severity'],
'promotions' => ['title', 'description', 'offer_type'],
default => ['id'],
};
$query->where(function ($inner) use ($columns, $search) {
foreach ($columns as $column) {
$inner->orWhere($column, 'like', '%'.$search.'%');
}
});
}
private function tableFor(string $resource): string
{
if (! array_key_exists($resource, self::RESOURCE_TABLES)) {
abort(404, 'Unknown Essentials resource.');
}
return self::RESOURCE_TABLES[$resource];
}
private function idKey(string $resource): string
{
return rtrim($resource, 's').'_id';
}
private function hasSoftDeletes(string $resource): bool
{
return in_array($resource, ['categories', 'vendors', 'listings', 'requests'], true);
}
private function orderColumn(string $resource): string
{
return $resource === 'reviews' ? 'created_at' : 'updated_at';
}
private function defaultCategoryId(): string
{
$category = DB::table('essentials_categories')->whereNull('deleted_at')->orderBy('sort_order')->first();
return (string) ($category->id ?? '00000000-0000-7000-8000-000000003001');
}
private function slug(mixed $value): string
{
$slug = strtolower(trim(preg_replace('/[^a-z0-9]+/i', '-', (string) $value), '-'));
return $slug !== '' ? $slug : strtolower(substr($this->ids->generate(), -12));
}
private function jsonOrNull(mixed $value): ?string
{
if ($value === null || $value === '') {
return null;
}
return is_string($value) ? $value : json_encode($value);
}
}