File: /home/xedaptot/be.naniguide.com/app/Model/PlanRemoteMapping.php
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
use App\Library\Facades\Billing;
use App\Library\Traits\HasUid;
use App\Cashier\DTO\RemotePlanDTO;
use App\Cashier\Contracts\RemoteSubscriptionGatewayInterface;
class PlanRemoteMapping extends Model
{
use HasUid;
protected $table = 'plan_remote_mappings';
protected $fillable = [
'plan_id',
'payment_gateway_id',
'remote_plan_id',
'remote_plan_name',
'remote_price',
'remote_currency',
'remote_interval_count',
'remote_interval_unit',
'remote_trial_days',
'remote_metadata',
'last_synced_at',
];
protected $casts = [
'remote_metadata' => 'array',
'remote_price' => 'float',
'remote_trial_days' => 'integer',
'last_synced_at' => 'datetime',
];
public function plan()
{
return $this->belongsTo(Plan::class);
}
public function paymentGateway()
{
return $this->belongsTo(PaymentGateway::class);
}
/**
* Get all mismatches between local plan and remote plan.
* Returns an array of mismatch descriptions. Empty = all good.
*/
public function getMismatches(): array
{
$plan = $this->plan;
$mismatches = [];
// Price
if ($this->remote_price !== null && abs($plan->price - $this->remote_price) > 0.01) {
$mismatches[] = [
'field' => 'price',
'local' => $plan->price,
'remote' => $this->remote_price,
'message' => "Price mismatch: local {$plan->price} vs remote {$this->remote_price}",
];
}
// Currency
$localCurrency = strtoupper($plan->currency->code ?? '');
$remoteCurrency = strtoupper($this->remote_currency ?? '');
if ($remoteCurrency && $localCurrency !== $remoteCurrency) {
$mismatches[] = [
'field' => 'currency',
'local' => $localCurrency,
'remote' => $remoteCurrency,
'message' => "Currency mismatch: local {$localCurrency} vs remote {$remoteCurrency}",
];
}
// Interval count
if ($this->remote_interval_count !== null && (int)$plan->frequency_amount !== (int)$this->remote_interval_count) {
$mismatches[] = [
'field' => 'interval_count',
'local' => $plan->frequency_amount,
'remote' => $this->remote_interval_count,
'message' => "Interval count mismatch: local {$plan->frequency_amount} vs remote {$this->remote_interval_count}",
];
}
// Interval unit
if ($this->remote_interval_unit && $plan->frequency_unit !== $this->remote_interval_unit) {
$mismatches[] = [
'field' => 'interval_unit',
'local' => $plan->frequency_unit,
'remote' => $this->remote_interval_unit,
'message' => "Interval unit mismatch: local {$plan->frequency_unit} vs remote {$this->remote_interval_unit}",
];
}
// Trial days
$localTrialDays = 0;
if ($plan->hasTrial() && $plan->trial_amount) {
$localTrialDays = match ($plan->trial_unit) {
'day' => (int) $plan->trial_amount,
'week' => (int) $plan->trial_amount * 7,
'month' => (int) $plan->trial_amount * 30,
'year' => (int) $plan->trial_amount * 365,
default => 0,
};
}
$remoteTrialDays = (int)($this->remote_trial_days ?? 0);
if ($remoteTrialDays !== $localTrialDays) {
$mismatches[] = [
'field' => 'trial_days',
'local' => $localTrialDays,
'remote' => $remoteTrialDays,
'message' => "Trial days mismatch: local {$localTrialDays} vs remote {$remoteTrialDays}",
];
}
return $mismatches;
}
/**
* Validate that local plan matches remote plan.
* Throws exception with ALL mismatches listed (not just the first one).
*
* @throws \Exception if any mismatch found
*/
public function validateMapping(): void
{
$mismatches = $this->getMismatches();
if (!empty($mismatches)) {
$details = array_map(fn($m) => $m['message'], $mismatches);
throw new \Exception(
"Plan mapping mismatch for '{$this->plan->name}' ↔ '{$this->remote_plan_name}' " .
"(gateway: {$this->paymentGateway->name}): " .
implode('; ', $details)
);
}
}
/**
* Check if local plan is in sync with remote plan data.
*/
public function isInSync(): bool
{
return empty($this->getMismatches());
}
/**
* Sync metadata from remote provider
*/
public function syncFromRemote(RemotePlanDTO $remotePlan): void
{
$this->remote_plan_name = $remotePlan->name;
$this->remote_price = $remotePlan->price;
$this->remote_currency = $remotePlan->currency;
$this->remote_interval_count = $remotePlan->intervalCount;
$this->remote_interval_unit = $remotePlan->intervalUnit;
$this->remote_trial_days = $remotePlan->trialDays;
$this->remote_metadata = $remotePlan->metadata;
$this->last_synced_at = now();
$this->save();
}
/**
* Get a comparison summary: local vs remote for logging/display.
*/
public function getComparisonSummary(): array
{
$plan = $this->plan;
$localTrialDays = ($plan->hasTrial() && $plan->trial_unit === 'day')
? (int)$plan->trial_amount : 0;
return [
'local_plan' => $plan->name,
'remote_plan' => $this->remote_plan_name,
'gateway' => $this->paymentGateway->name,
'gateway_type' => $this->paymentGateway->type,
'fields' => [
'price' => ['local' => $plan->price, 'remote' => $this->remote_price],
'currency' => ['local' => strtoupper($plan->currency->code ?? ''), 'remote' => strtoupper($this->remote_currency ?? '')],
'interval_count' => ['local' => $plan->frequency_amount, 'remote' => $this->remote_interval_count],
'interval_unit' => ['local' => $plan->frequency_unit, 'remote' => $this->remote_interval_unit],
'trial_days' => ['local' => $localTrialDays, 'remote' => $this->remote_trial_days ?? 0],
],
'mismatches' => $this->getMismatches(),
'in_sync' => $this->isInSync(),
];
}
/**
* Get the service instance for this mapping's gateway
*/
public function getService(): RemoteSubscriptionGatewayInterface
{
$service = Billing::resolveService($this->paymentGateway);
if (!($service instanceof RemoteSubscriptionGatewayInterface)) {
throw new \Exception(
"Gateway '{$this->paymentGateway->name}' does not support remote subscriptions"
);
}
return $service;
}
}