Commit 724a37ca authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(subscriptions): auto-sync on member activation + syncForMember method

- Add syncForMember(memberId) to SubscriptionSyncService: creates the
  member's own FY subscription row + syncs all currently-active dependents
  in one call. Uses same rate/discount/dedup logic as the batch generator.
- Wire member.activated EventBus listener in Subscriptions bootstrap so
  any newly-activated member (and their dependents) gets a subscription row
  immediately, without waiting for the next annual batch generation.
- Existing syncForDependent is unchanged; syncForMember delegates to it.
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent c492f45a
...@@ -8,14 +8,107 @@ use App\Core\Logger; ...@@ -8,14 +8,107 @@ use App\Core\Logger;
use App\Modules\Rules\Services\RuleEngine; use App\Modules\Rules\Services\RuleEngine;
/** /**
* Adds a subscription row for a newly-activated dependent (spouse / child / temporary) * Adds subscription rows for newly-activated members and their dependents.
* for the current financial year.
* *
* Called from Subscriptions bootstrap on spouse.fee_paid, child.fee_paid, * syncForMember — called on member.activated; creates the member's own FY row + all active dependents
* temporary.fee_paid, and on *.added when the dependent is immediately active (fee = 0). * syncForDependent — called on spouse/child/temporary.fee_paid and *.added (fee=0)
*/ */
final class SubscriptionSyncService final class SubscriptionSyncService
{ {
/**
* Create subscription rows for a newly-activated member and all their currently-active dependents.
* Safe to call multiple times — dedup guard prevents duplicate rows.
*/
public static function syncForMember(int $memberId): void
{
try {
$db = App::getInstance()->db();
$member = $db->selectOne(
"SELECT id, full_name_ar, membership_type, status FROM members WHERE id = ? AND is_archived = 0",
[$memberId]
);
if (!$member || $member['status'] !== 'active') {
return;
}
$exemptTypes = self::getExemptTypes();
if (in_array($member['membership_type'], $exemptTypes, true)) {
return;
}
$fy = financial_year();
$yearSuffix = explode('/', $fy)[0] ?? '';
$today = date('Y-m-d');
$ts = date('Y-m-d H:i:s');
$employee = App::getInstance()->currentEmployee();
$empId = $employee ? (int) $employee->id : null;
// ── Member's own subscription row ─────────────────────────────
$existing = $db->selectOne(
"SELECT id FROM subscriptions WHERE member_id = ? AND financial_year = ? AND person_type = 'member'",
[$memberId, $fy]
);
if (!$existing) {
$rateMap = [
'SVC_ANNUAL_MEMBER_' . $yearSuffix,
'SVC_ANNUAL_MEMBER',
];
$rate = '';
foreach ($rateMap as $code) {
$rate = self::getRate($code);
if ($rate !== '') break;
}
if ($rate === '') $rate = '492.00';
$yearAdjustment = RuleEngine::get('SUBSCRIPTION_YEAR_ADJUSTMENT_' . $yearSuffix);
$discountPct = $yearAdjustment['discount_percentage'] ?? null;
$discount = $discountPct ? bcdiv(bcmul($rate, $discountPct, 4), '100', 2) : '0.00';
$total = bcsub($rate, $discount, 2);
$devFeeRule = RuleEngine::get('DEVELOPMENT_FEE');
$devFee = (string) ($devFeeRule['amount'] ?? '35.00');
$db->insert('subscriptions', [
'member_id' => $memberId,
'financial_year' => $fy,
'person_type' => 'member',
'person_id' => null,
'person_name' => $member['full_name_ar'],
'base_amount' => $rate,
'development_fee' => $devFee,
'discount_amount' => $discount,
'total_amount' => $total,
'status' => 'pending',
'created_at' => $ts,
'updated_at' => $ts,
'created_by' => $empId,
]);
Logger::info("SubscriptionSyncService: added member #{$memberId} to FY {$fy}");
}
// ── All active dependents ─────────────────────────────────────
$dependentMap = [
'spouse' => ['table' => 'spouses', 'id_col' => 'id'],
'child' => ['table' => 'children', 'id_col' => 'id'],
'temporary' => ['table' => 'temporary_members', 'id_col' => 'id'],
];
foreach ($dependentMap as $personType => $cfg) {
$deps = $db->select(
"SELECT id, full_name_ar FROM {$cfg['table']} WHERE member_id = ? AND is_archived = 0 AND status = 'active'",
[$memberId]
);
foreach ($deps as $dep) {
self::syncForDependent($personType, (int) $dep['id'], $memberId);
}
}
} catch (\Throwable $e) {
Logger::error("SubscriptionSyncService::syncForMember failed: " . $e->getMessage(), [
'member_id' => $memberId,
]);
}
}
/** /**
* @param string $personType 'spouse' | 'child' | 'temporary' * @param string $personType 'spouse' | 'child' | 'temporary'
* @param int $personId PK in the respective table * @param int $personId PK in the respective table
......
...@@ -14,7 +14,15 @@ PermissionRegistry::register('subscriptions', [ ...@@ -14,7 +14,15 @@ PermissionRegistry::register('subscriptions', [
'subscription.generate_batch' => ['ar' => 'توليد اشتراكات', 'en' => 'Generate Batch'], 'subscription.generate_batch' => ['ar' => 'توليد اشتراكات', 'en' => 'Generate Batch'],
]); ]);
// ─── Auto-sync subscriptions when a dependent is activated ─────────────────── // ─── Auto-sync subscriptions when a member or dependent is activated ─────────
// New member activation → create member subscription row + all active dependents
EventBus::listen('member.activated', function (array $data) {
$memberId = (int) ($data['member_id'] ?? 0);
if ($memberId) {
SubscriptionSyncService::syncForMember($memberId);
}
});
// Path A: fee already paid through Cashier → dependent just became active // Path A: fee already paid through Cashier → dependent just became active
EventBus::listen('spouse.fee_paid', function (array $data) { EventBus::listen('spouse.fee_paid', function (array $data) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment