File size: 18,842 Bytes
3bdbcea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
<?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);
    }
}