352 lines
15 KiB
PHP
352 lines
15 KiB
PHP
<?php
|
|
namespace App\Services;
|
|
|
|
use App\Events\DataBatchUpdated;
|
|
use App\Models\Campaign;
|
|
use App\Models\Category;
|
|
use App\Models\Customer;
|
|
use App\Models\CustomerAddress;
|
|
use App\Models\Order;
|
|
use App\Models\OrderItem;
|
|
use App\Models\Outlet;
|
|
use App\Models\OutletCampaign;
|
|
use App\Models\OutletInventory;
|
|
use App\Models\Product;
|
|
use App\Models\ProductLike;
|
|
use App\Models\ProductReview;
|
|
use App\Models\ProductVariant;
|
|
use App\Models\Purchase;
|
|
use App\Models\PurchaseItem;
|
|
use App\Models\RawMaterial;
|
|
use App\Models\ReceiptItem;
|
|
use App\Models\StockCard;
|
|
use App\Models\Supplier;
|
|
use App\Models\SyncDelete;
|
|
use App\Models\Tenant;
|
|
use App\Models\TenantPaymentMethod;
|
|
use App\Models\Transaction;
|
|
use App\Models\Unit;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
class SyncService
|
|
{
|
|
protected static $columnCache = [];
|
|
protected static $softDeleteCache = [];
|
|
protected array $tableMap = [
|
|
'campaigns' => Campaign::class,
|
|
'categories' => Category::class,
|
|
'customers' => Customer::class,
|
|
'customer_address' => CustomerAddress::class,
|
|
'orders' => Order::class,
|
|
'order_items' => OrderItem::class,
|
|
'outlets' => Outlet::class,
|
|
'outlet_campaigns' => OutletCampaign::class,
|
|
'outlet_inventories' => OutletInventory::class,
|
|
'products' => Product::class,
|
|
'product_likes' => ProductLike::class,
|
|
'product_reviews' => ProductReview::class,
|
|
'product_variants' => ProductVariant::class,
|
|
'purchases' => Purchase::class,
|
|
'purchase_items' => PurchaseItem::class,
|
|
'raw_materials' => RawMaterial::class,
|
|
'receipt_items' => ReceiptItem::class,
|
|
'stock_cards' => StockCard::class,
|
|
'suppliers' => Supplier::class,
|
|
'sync_deletes' => SyncDelete::class,
|
|
'tenants' => Tenant::class,
|
|
'tenant_payment_methods'=> TenantPaymentMethod::class,
|
|
'transactions' => Transaction::class,
|
|
'units' => Unit::class,
|
|
'users' => User::class,
|
|
];
|
|
|
|
public function getDeltaSync($lastSync)
|
|
{
|
|
$tenantId = Auth::user()->tenant_id;
|
|
return [
|
|
'tenants' => $this->tableMap['tenants']::withoutGlobalScopes()
|
|
->where('uuid', $tenantId)
|
|
->where('updated_at', '>', $lastSync)
|
|
->first(),
|
|
'tenant_payment_methods' => $this->tableMap['tenant_payment_methods']::where('updated_at', '>', $lastSync)->get(),
|
|
'campaigns' => $this->tableMap['campaigns']::withTrashed()->where('updated_at', '>', $lastSync)->get(),
|
|
'outlets' => $this->tableMap['outlets']::withTrashed()->where('updated_at', '>', $lastSync)->get(),
|
|
'outlet_campaigns' => $this->tableMap['outlet_campaigns']::withTrashed()->where('updated_at', '>', $lastSync)->get(),
|
|
'users' => $this->tableMap['users']::withTrashed()->where('updated_at', '>', $lastSync)->get(),
|
|
'units' => $this->tableMap['units']::where('updated_at', '>', $lastSync)->get(),
|
|
'categories' => $this->tableMap['categories']::where('updated_at', '>', $lastSync)->get(),
|
|
'products' => $this->tableMap['products']::withTrashed()->where('updated_at', '>', $lastSync)->get(),
|
|
'product_variants' => $this->tableMap['product_variants']::withTrashed()->where('updated_at', '>', $lastSync)->get(),
|
|
'raw_materials' => $this->tableMap['raw_materials']::withTrashed()->where('updated_at', '>', $lastSync)->get(),
|
|
'receipt_items' => $this->tableMap['receipt_items']::withTrashed()->where('updated_at', '>', $lastSync)->get(),
|
|
'outlet_inventories' => $this->tableMap['outlet_inventories']::withTrashed()->where('updated_at', '>', $lastSync)->get(),
|
|
'customers' => $this->tableMap['customers']::withTrashed()->where('updated_at', '>', $lastSync)->get(),
|
|
'customer_address' => $this->tableMap['customer_address']::withTrashed()->where('updated_at', '>', $lastSync)->get(),
|
|
'product_likes' => $this->tableMap['product_likes']::where('updated_at', '>', $lastSync)->get(),
|
|
'product_reviews' => $this->tableMap['product_reviews']::where('updated_at', '>', $lastSync)->get(),
|
|
'orders' => $this->tableMap['orders']::where('updated_at', '>', $lastSync)->get(),
|
|
'order_items' => $this->tableMap['order_items']::with(['variant' => function($q) {
|
|
$q->select('uuid','product_id')->withTrashed();
|
|
}])->where('updated_at', '>', $lastSync)->get(),
|
|
'suppliers' => $this->tableMap['suppliers']::withTrashed()->where('updated_at', '>', $lastSync)->get(),
|
|
'purchases' => $this->tableMap['purchases']::where('updated_at', '>', $lastSync)->get(),
|
|
'purchase_items' => $this->tableMap['purchase_items']::with(['item' => function (MorphTo $morphTo) {
|
|
$morphTo->constrain([
|
|
ProductVariant::class => fn($q) =>
|
|
$q->select('uuid', 'product_id')->withTrashed(),
|
|
|
|
RawMaterial::class => fn($q) =>
|
|
$q->select('uuid')->withTrashed(),
|
|
]);
|
|
}
|
|
])->where('updated_at', '>', $lastSync)->get(),
|
|
'transactions' => $this->tableMap['transactions']::where('updated_at', '>', $lastSync)->get(),
|
|
'stock_cards' => $this->tableMap['stock_cards']::where('created_at', '>', $lastSync)->get(),
|
|
'sync_deletes' => $this->tableMap['sync_deletes']::where('created_at', '>', $lastSync)->get()
|
|
];
|
|
}
|
|
|
|
public function updateServerTime(Carbon $currentTime)
|
|
{
|
|
Auth::user()->update(['last_sync' => $currentTime]);
|
|
}
|
|
|
|
public function processBatchSync(array $payload)
|
|
{
|
|
$items = $payload['items'];
|
|
$results = ['success_count' => 0, 'ignored_count' => 0, 'errors' => []];
|
|
$changedTables = [];
|
|
|
|
foreach ($items as $item) {
|
|
try {
|
|
$tableName = $item['table'];
|
|
$uuid = $item['local_id'];
|
|
$action = $item['action'];
|
|
$data = $item['data'];
|
|
|
|
$modelClass = $this->tableMap[$tableName] ?? null;
|
|
|
|
if (!$modelClass) throw new \Exception("Table {$tableName} not mapped.");
|
|
|
|
DB::transaction(function () use ($modelClass, $uuid, $data, $action, $tableName, &$results, &$changedTables) {
|
|
if ($action === 'bulk_update') {
|
|
if($tableName === 'product_variants') {
|
|
$visibleIds = $data['visible_ids'] ?? [];
|
|
|
|
$modelClass::whereNotIn('uuid', $visibleIds)
|
|
->update(['is_visible_online' => false, 'updated_at' => $data['updated_at']]);
|
|
|
|
$modelClass::whereIn('uuid', $visibleIds)
|
|
->update(['is_visible_online' => true, 'updated_at' => $data['updated_at']]);
|
|
|
|
$results['success_count']++;
|
|
$changedTables[] = 'product_variants';
|
|
return;
|
|
}
|
|
|
|
if($tableName === 'outlet_campaigns') {
|
|
$ids = $data['ids'] ?? [];
|
|
|
|
$modelClass::whereIn('uuid', $ids)
|
|
->update(['deleted_at' => $data['deleted_at']]);
|
|
|
|
$results['success_count']++;
|
|
$changedTables = 'outlet_campaigns';
|
|
return;
|
|
}
|
|
}
|
|
|
|
$usesSoftDeletes = $this->usesSoftDeletes($modelClass);
|
|
|
|
if($tableName === 'tenants') {
|
|
$query = $modelClass::withoutGlobalScopes()->where('uuid', $uuid);
|
|
} else {
|
|
$query = $modelClass::where('uuid', $uuid);
|
|
}
|
|
|
|
if (in_array($tableName, ['product_variants','outlet_inventories', 'stock_cards'])) {
|
|
$query->lockForUpdate();
|
|
}
|
|
|
|
if($usesSoftDeletes) {
|
|
$query->withTrashed();
|
|
}
|
|
|
|
$existing = $query->first();
|
|
|
|
$deltaTotalSold = 0;
|
|
if ($tableName === 'product_variants' && isset($data['qty_sold'])) {
|
|
$deltaTotalSold = (float) $data['qty_sold'];
|
|
unset($data['qty_sold']);
|
|
}
|
|
|
|
$shouldUpdateMetadata = true;
|
|
if ($existing && isset($data['updated_at'])) {
|
|
if (Carbon::parse($data['updated_at'])->lte($existing->updated_at)) {
|
|
$shouldUpdateMetadata = false;
|
|
$results['ignored_count']++;
|
|
}
|
|
}
|
|
|
|
if($shouldUpdateMetadata) {
|
|
if (strtoupper($action) === 'DELETE') {
|
|
$this->handleSoftDelete($modelClass, $uuid, $data);
|
|
} else {
|
|
if ($tableName === 'stock_cards') {
|
|
$this->handleDeltaStock($data);
|
|
$modelClass::create($data);
|
|
} else {
|
|
if($tableName === 'outlets' && isset($data['is_main_outlet']) && $data['is_main_outlet'] === true) {
|
|
Outlet::whereNot('uuid', $uuid)->update(['is_main_outlet' => false]);
|
|
}
|
|
|
|
$modelClass::updateOrCreate(['uuid' => $uuid], $data);
|
|
|
|
}
|
|
}
|
|
$results['success_count']++;
|
|
}
|
|
|
|
if ($deltaTotalSold != 0) {
|
|
$targetRecord = $existing ?? $modelClass::where('uuid', $uuid)->first();
|
|
|
|
if ($targetRecord) {
|
|
$targetRecord->increment('total_sold', $deltaTotalSold);
|
|
}
|
|
}
|
|
|
|
if(!in_array($tableName, $changedTables)) {
|
|
$changedTables[] = $tableName;
|
|
}
|
|
|
|
}, 3);
|
|
} catch (\Exception $e) {
|
|
$results['errors'][] = [
|
|
'table_name' => $item['table'],
|
|
'local_id' => $item['local_id'],
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
if($results['success_count'] > 0 && !empty($changedTables)) {
|
|
$masterTables = [
|
|
'tenants',
|
|
'tenant_payment_methods',
|
|
'outlets',
|
|
'campaigns',
|
|
'categories',
|
|
'products',
|
|
'product_variants',
|
|
'outlet_inventories',
|
|
'orders'
|
|
];
|
|
|
|
$relevantChanges = array_intersect($changedTables, $masterTables);
|
|
|
|
$tenantId = Auth::user()->tenant_id;
|
|
broadcast(new DataBatchUpdated($tenantId, array_values($relevantChanges)))->toOthers();
|
|
}
|
|
return $results;
|
|
}
|
|
|
|
protected function tableHasColumn(string $table, string $column): bool
|
|
{
|
|
$cacheKey = "table_columns_{$table}";
|
|
|
|
if (isset(static::$columnCache[$table])) {
|
|
return in_array($column, static::$columnCache[$table]);
|
|
}
|
|
|
|
$columns = Cache::remember($cacheKey, now()->addDay(), function () use ($table) {
|
|
return Schema::getColumnListing($table);
|
|
});
|
|
|
|
static::$columnCache[$table] = $columns;
|
|
|
|
return in_array($column, $columns);
|
|
}
|
|
|
|
protected function usesSoftDeletes($modelClass): bool
|
|
{
|
|
if (isset(static::$softDeleteCache[$modelClass])) {
|
|
return static::$softDeleteCache[$modelClass];
|
|
}
|
|
|
|
$uses = in_array(
|
|
'Illuminate\Database\Eloquent\SoftDeletes',
|
|
class_uses_recursive($modelClass)
|
|
);
|
|
|
|
static::$softDeleteCache[$modelClass] = $uses;
|
|
return $uses;
|
|
}
|
|
|
|
private function handleDeltaStock($data)
|
|
{
|
|
$inventory = OutletInventory::withTrashed()
|
|
->lockForUpdate()
|
|
->where('uuid', '=', $data['outlet_inventory_id'])
|
|
->first();
|
|
|
|
if (!$inventory) {
|
|
throw new \Exception("inventory with id {$data['outlet_inventory_id']} is not found for stock synchronize");
|
|
}
|
|
|
|
if($inventory->stock_type === 'unlimited') return;
|
|
|
|
if(isset($data['type']) && isset($data['stock_type']) && $data['stock_type'] === 'fixed') {
|
|
$type = $data['type'];
|
|
if (in_array($type, ['adjustment_in', 'adjustment_out'])) {
|
|
$inventory->stock = $data['current_stock'];
|
|
$inventory->updated_at = $data['created_at'];
|
|
$inventory->save();
|
|
} else if(in_array($type, ['sale', 'transfer_out'])) {
|
|
$inventory->decrement('stock', $data['qty_change']);
|
|
} else if(in_array($type, ['production', 'purchase', 'transfer_in'])) {
|
|
$inventory->increment('stock', $data['qty_change']);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private function handleSoftDelete($modelClass, $uuid, $data)
|
|
{
|
|
$tableName = (new $modelClass())->getTable();
|
|
$deletedAt = isset($data['deleted_at']) ? Carbon::parse($data['deleted_at']) : now();
|
|
$query = $modelClass::withTrashed();
|
|
|
|
if (empty($data) || (!isset($data['uuid']) && count($data) === 1)) {
|
|
$query->where('uuid', $uuid);
|
|
} else {
|
|
foreach ($data as $key => $value) {
|
|
if (in_array($key, ['is_active', 'is_delete_by_owner', 'deleted_at'])) continue;
|
|
$query->where($key, $value);
|
|
}
|
|
}
|
|
|
|
$records = $query->get();
|
|
|
|
if ($records->isNotEmpty()) {
|
|
foreach ($records as $record) {
|
|
if (isset($data['is_delete_by_owner']) && $this->tableHasColumn($tableName, 'is_delete_by_owner')) {
|
|
$record->is_delete_by_owner = $data['is_delete_by_owner'];
|
|
}
|
|
|
|
if(isset($data['is_active']) && $this->tableHasColumn($tableName, 'is_active')) {
|
|
$record->is_active = $data['is_active'];
|
|
}
|
|
$record->deleted_at = $deletedAt;
|
|
$record->save();
|
|
$record->delete();
|
|
$record->updateQuietly(['deleted_at' => $deletedAt]);
|
|
}
|
|
}
|
|
}
|
|
} |