Commit 01e3b4ef authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(pricing,members): merge board-offer discounts into the member special-discount dropdown

Client clarified their earlier request: the member "special discount"
dropdown should show board-approved special_discounts AND active
عروض مجلس الإدارة (board_offers) cash discounts side by side, not one
instead of the other — they'd stopped seeing anything they'd added
under Board Offers.

- Add members.special_discount_source ('special_discount'|'board_offer')
  and drop the hard FK on special_discount_id (a single column can no
  longer FK exactly one table). Integrity is now validated in
  MemberController::parseDiscountSelection().
- BoardOffer::allActiveWithCashDiscount() surfaces board offers that
  define a cash discount as selectable options.
- SpecialDiscountService::resolveAssignedDiscount()/amountForType() give
  one place that normalizes "the member's assigned discount" across both
  source tables — used by BillingService's invoice line item, the show
  page's applied-discount banner, and the dropdown's own validation.
- fill-form/edit/show views render two <optgroup>s ("عروض مجلس الإدارة" /
  "الخصومات الخاصة") with prefixed option values (bo:<id> / sd:<id>) so a
  single form field can select from either table; edit.php's live
  discount-amount preview now handles fixed-amount discounts too, not
  just percentage.
Co-Authored-By: 's avatarClaude Sonnet 5 <noreply@anthropic.com>
parent 67196669
......@@ -28,7 +28,7 @@ class Member extends Model
'area', 'governorate', 'correspondence_address',
'employment_type', 'occupation', 'job_title', 'employment_date',
'business_address', 'office_phone', 'office_fax', 'business_activity',
'membership_value', 'special_discount_id', 'special_discount_document', 'discount_amount',
'membership_value', 'special_discount_id', 'special_discount_source', 'special_discount_document', 'discount_amount',
'payment_method', 'referral_source', 'referred_by_representative_id', 'photo_path',
'workflow_instance_id',
];
......
......@@ -7,6 +7,7 @@ use App\Core\App;
use App\Modules\ServiceCatalog\Models\ServicePrice;
use App\Modules\Members\Services\BoardOfferService;
use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Pricing\Services\SpecialDiscountService;
/**
* Calculates the TOTAL bill for a member including all additions.
......@@ -422,28 +423,17 @@ final class BillingService
'category' => 'required',
];
// ── 2b. Special Discount ──
// ── 2b. Special Discount (from either special_discounts or board_offers — see
// SpecialDiscountService::resolveAssignedDiscount) ──
if (!empty($member['special_discount_id'])) {
$discountRow = $db->selectOne(
"SELECT * FROM special_discounts sd WHERE sd.id = ? AND sd.is_active = 1",
[(int) $member['special_discount_id']]
);
if ($discountRow) {
$today = date('Y-m-d');
$inRange = (empty($discountRow['effective_from']) || $today >= $discountRow['effective_from'])
&& (empty($discountRow['effective_to']) || $today <= $discountRow['effective_to']);
if ($inRange) {
$discType = $discountRow['discount_type'] ?? 'percentage';
if ($discType === 'percentage') {
$discountAmount = $member['discount_amount'] ?? bcdiv(bcmul($membershipValue, $discountRow['discount_percentage'], 4), '100', 2);
$discountLabel = 'خصم خاص: ' . $discountRow['name_ar'] . ' (' . $discountRow['discount_percentage'] . '%)';
} elseif ($discType === 'fixed_amount') {
$discountAmount = $discountRow['fixed_amount'] ?? '0.00';
$discountLabel = 'خصم خاص: ' . $discountRow['name_ar'] . ' (' . money($discountAmount) . ')';
} else {
$discountAmount = '0.00';
$discountLabel = 'خصم خاص: ' . $discountRow['name_ar'];
}
$assigned = SpecialDiscountService::resolveAssignedDiscount($member);
if ($assigned) {
$discountAmount = ($assigned['discount_type'] === 'percentage' && !empty($member['discount_amount']))
? $member['discount_amount']
: SpecialDiscountService::amountForType($assigned['discount_type'], $assigned['discount_value'], $membershipValue);
$discountLabel = $assigned['discount_type'] === 'percentage'
? $assigned['label'] . ' (' . $assigned['discount_value'] . '%)'
: $assigned['label'] . ' (' . money($discountAmount) . ')';
if (bccomp($discountAmount, '0', 2) > 0) {
$items[] = [
......@@ -456,7 +446,7 @@ final class BillingService
];
}
$bonusYears = (int) ($discountRow['bonus_free_subscription_years'] ?? 0);
$bonusYears = $assigned['bonus_free_subscription_years'];
if ($bonusYears > 0) {
$items[] = [
'type' => 'bonus_info',
......@@ -470,7 +460,6 @@ final class BillingService
}
}
}
}
// ── 2c. Regulatory Discount ──
if (!empty($member['regulatory_discount_id'])) {
......
......@@ -94,7 +94,8 @@
</div>
<!-- Special Discount -->
<?php if (!empty($specialDiscounts)): ?>
<?php if (!empty($specialDiscounts) || !empty($boardOfferDiscounts)): ?>
<?php $currentDiscountSource = $member->special_discount_source ?? 'special_discount'; ?>
<div class="card" style="margin-bottom:20px;padding:20px;border-right:4px solid #D97706;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:15px;">
<i data-lucide="percent" style="width:18px;height:18px;color:#D97706;"></i>
......@@ -105,14 +106,32 @@
<label class="form-label">نوع الخصم الخاص</label>
<select name="special_discount_id" id="special_discount_select" class="form-select">
<option value="">— بدون خصم —</option>
<?php if (!empty($boardOfferDiscounts)): ?>
<optgroup label="عروض مجلس الإدارة">
<?php foreach ($boardOfferDiscounts as $bo): ?>
<option value="bo:<?= (int) $bo['id'] ?>"
data-type="<?= e($bo['cash_discount_type']) ?>"
data-value="<?= e($bo['cash_discount_value']) ?>"
data-doc="0"
<?= ($currentDiscountSource === 'board_offer' && ((int) ($member->special_discount_id ?? 0)) === (int) $bo['id']) ? 'selected' : '' ?>>
<?= e($bo['title_ar']) ?> (<?= $bo['cash_discount_type'] === 'percentage' ? e($bo['cash_discount_value']) . '%' : money($bo['cash_discount_value']) ?>)
</option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
<?php if (!empty($specialDiscounts)): ?>
<optgroup label="الخصومات الخاصة">
<?php foreach ($specialDiscounts as $sd): ?>
<option value="<?= (int) $sd['id'] ?>"
data-pct="<?= e($sd['discount_percentage']) ?>"
<option value="sd:<?= (int) $sd['id'] ?>"
data-type="<?= e($sd['discount_type'] ?? 'percentage') ?>"
data-value="<?= ($sd['discount_type'] ?? 'percentage') === 'fixed_amount' ? e($sd['fixed_amount']) : e($sd['discount_percentage']) ?>"
data-doc="<?= (int) $sd['requires_document'] ?>"
<?= ((int) ($member->special_discount_id ?? 0)) === (int) $sd['id'] ? 'selected' : '' ?>>
<?= e($sd['name_ar']) ?> (<?= e($sd['discount_percentage']) ?>%)
<?= ($currentDiscountSource === 'special_discount' && ((int) ($member->special_discount_id ?? 0)) === (int) $sd['id']) ? 'selected' : '' ?>>
<?= e($sd['name_ar']) ?> (<?= ($sd['discount_type'] ?? 'percentage') === 'fixed_amount' ? money($sd['fixed_amount']) : e($sd['discount_percentage']) . '%' ?>)
</option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
</select>
</div>
<div class="form-group" id="discount-info" style="display:<?= $member->special_discount_id ? 'block' : 'none' ?>;">
......@@ -237,10 +256,11 @@ document.addEventListener('DOMContentLoaded', function() {
docGroup.style.display = 'none';
return;
}
var pct = parseFloat(opt.getAttribute('data-pct') || '0');
var type = opt.getAttribute('data-type') || 'percentage';
var value = parseFloat(opt.getAttribute('data-value') || '0');
var needsDoc = parseInt(opt.getAttribute('data-doc') || '0');
var val = parseFloat(membershipValue) || 0;
var discountAmt = (val * pct / 100).toFixed(2);
var discountAmt = (type === 'fixed_amount' ? value : (val * value / 100)).toFixed(2);
amountDiv.textContent = discountAmt + ' ج.م';
infoDiv.style.display = 'block';
docGroup.style.display = needsDoc ? 'block' : 'none';
......
......@@ -297,7 +297,8 @@
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- القسم 6: خصم خاص (اختياري) -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<?php if (!empty($specialDiscounts)): ?>
<?php if (!empty($specialDiscounts) || !empty($boardOfferDiscounts)): ?>
<?php $currentDiscountSource = $member->special_discount_source ?? 'special_discount'; ?>
<div class="card" style="margin-bottom:20px;border-right:4px solid #D97706;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#D97706;">خصم خاص (اختياري)</h3>
......@@ -307,9 +308,20 @@
<label class="form-label">نوع الخصم الخاص</label>
<select name="special_discount_id" class="form-select">
<option value="">— بدون خصم —</option>
<?php if (!empty($boardOfferDiscounts)): ?>
<optgroup label="عروض مجلس الإدارة">
<?php foreach ($boardOfferDiscounts as $bo): ?>
<option value="bo:<?= (int) $bo['id'] ?>" <?= ($currentDiscountSource === 'board_offer' && ($member->special_discount_id ?? 0) == $bo['id']) ? 'selected' : '' ?>><?= e($bo['title_ar']) ?> (<?= $bo['cash_discount_type'] === 'percentage' ? e($bo['cash_discount_value']) . '%' : money($bo['cash_discount_value']) ?>)</option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
<?php if (!empty($specialDiscounts)): ?>
<optgroup label="الخصومات الخاصة">
<?php foreach ($specialDiscounts as $sd): ?>
<option value="<?= (int) $sd['id'] ?>" <?= ($member->special_discount_id ?? 0) == $sd['id'] ? 'selected' : '' ?>><?= e($sd['name_ar']) ?> (<?= e($sd['discount_percentage']) ?>%)</option>
<option value="sd:<?= (int) $sd['id'] ?>" <?= ($currentDiscountSource === 'special_discount' && ($member->special_discount_id ?? 0) == $sd['id']) ? 'selected' : '' ?>><?= e($sd['name_ar']) ?> (<?= ($sd['discount_type'] ?? 'percentage') === 'fixed_amount' ? money($sd['fixed_amount']) : e($sd['discount_percentage']) . '%' ?>)</option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
</select>
</div>
<div class="form-group">
......
......@@ -376,11 +376,11 @@ $_memberInitials = mb_substr($member->full_name_ar, 0, 2);
</div>
<!-- Special Discount Section -->
<?php if (!empty($availableDiscounts) && in_array($member->status, ['accepted', 'payment_pending']) && empty($pendingMembership)): ?>
<?php if ((!empty($availableDiscounts) || !empty($availableBoardOfferDiscounts)) && in_array($member->status, ['accepted', 'payment_pending']) && empty($pendingMembership)): ?>
<div style="padding:15px 20px;border-top:1px solid #E5E7EB;">
<?php if (!empty($specialDiscount)): ?>
<div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<span style="color:#059669;font-weight:600;font-size:13px;">&#x2705; خصم مُطبق: <?= e($specialDiscount['name_ar']) ?> (<?= e($specialDiscount['discount_percentage']) ?>%) = -<?= money($member->discount_amount ?? '0') ?></span>
<span style="color:#059669;font-weight:600;font-size:13px;">&#x2705; خصم مُطبق: <?= e($specialDiscount['label']) ?> (<?= $specialDiscount['discount_type'] === 'percentage' ? e($specialDiscount['discount_value']) . '%' : money($specialDiscount['discount_value']) ?>) = -<?= money($member->discount_amount ?? '0') ?></span>
<form method="POST" action="/members/<?= (int) $member->id ?>/apply-discount" style="display:inline;">
<?= csrf_field() ?>
<input type="hidden" name="special_discount_id" value="0">
......@@ -394,9 +394,20 @@ $_memberInitials = mb_substr($member->full_name_ar, 0, 2);
<label class="form-label" style="font-size:12px;">&#x1f3f7;&#xfe0f; خصم خاص</label>
<select name="special_discount_id" class="form-select" style="min-width:200px;" id="discountSelect" onchange="document.getElementById('discountDoc').style.display = this.selectedOptions[0].dataset.doc === '1' ? 'block' : 'none'">
<option value="">-- اختر خصم --</option>
<?php if (!empty($availableBoardOfferDiscounts)): ?>
<optgroup label="عروض مجلس الإدارة">
<?php foreach ($availableBoardOfferDiscounts as $offer): ?>
<option value="bo:<?= (int) $offer['id'] ?>" data-doc="0"><?= e($offer['title_ar']) ?> (<?= $offer['cash_discount_type'] === 'percentage' ? e($offer['cash_discount_value']) . '%' : money($offer['cash_discount_value']) ?>)</option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
<?php if (!empty($availableDiscounts)): ?>
<optgroup label="الخصومات الخاصة">
<?php foreach ($availableDiscounts as $disc): ?>
<option value="<?= (int) $disc['id'] ?>" data-doc="<?= $disc['requires_document'] ? '1' : '0' ?>"><?= e($disc['name_ar']) ?> (<?= e($disc['discount_percentage']) ?>%)</option>
<option value="sd:<?= (int) $disc['id'] ?>" data-doc="<?= $disc['requires_document'] ? '1' : '0' ?>"><?= e($disc['name_ar']) ?> (<?= ($disc['discount_type'] ?? 'percentage') === 'fixed_amount' ? money($disc['fixed_amount']) : e($disc['discount_percentage']) . '%' ?>)</option>
<?php endforeach; ?>
</optgroup>
<?php endif; ?>
</select>
</div>
<div class="form-group" style="margin:0;display:none;" id="discountDoc">
......
......@@ -41,6 +41,23 @@ class BoardOffer extends Model
);
}
/**
* Board offers selectable as a member's "special discount" — must define a cash discount
* (percentage or fixed amount), regardless of branch/installment settings.
*/
public static function allActiveWithCashDiscount(): array
{
$db = App::getInstance()->db();
$today = date('Y-m-d');
return $db->select(
"SELECT * FROM `board_offers`
WHERE `is_active` = 1 AND `cash_discount_type` IS NOT NULL AND `cash_discount_value` IS NOT NULL
AND `effective_from` <= ? AND `effective_to` >= ?
ORDER BY `title_ar`",
[$today, $today]
);
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
......
......@@ -24,17 +24,20 @@ final class SpecialDiscountService
return self::emptyResult();
}
// If the member has a specific discount assigned, use that
if (!empty($member['special_discount_id'])) {
$discount = $db->selectOne(
"SELECT * FROM special_discounts WHERE id = ? AND is_active = 1",
[(int) $member['special_discount_id']]
);
if ($discount) {
if (self::isWithinDateRange($discount, $today)) {
return self::calculateDiscount($discount, $membershipValue, $totalPaid);
}
}
// If the member has a specific discount assigned (from either special_discounts or
// board_offers — see resolveAssignedDiscount), use that.
$assigned = self::resolveAssignedDiscount($member);
if ($assigned !== null) {
$discountAmount = self::amountForType($assigned['discount_type'], $assigned['discount_value'], $membershipValue);
return [
'discount_amount' => $discountAmount,
'discount_label' => $assigned['label'],
'discount_id' => $assigned['id'],
'discount_type' => $assigned['discount_type'],
'percentage' => $assigned['discount_type'] === 'percentage' ? $assigned['discount_value'] : '0.00',
'bonus_free_years'=> $assigned['bonus_free_subscription_years'],
'applies_to' => $assigned['applies_to'],
];
}
// Check for auto-applicable conditional discounts (full_payment, min_payment)
......@@ -61,8 +64,11 @@ final class SpecialDiscountService
$db = App::getInstance()->db();
$today = date('Y-m-d');
$member = $db->selectOne("SELECT special_discount_id, membership_value FROM members WHERE id = ?", [$memberId]);
if (!$member || empty($member['special_discount_id'])) {
$member = $db->selectOne("SELECT special_discount_id, special_discount_source, membership_value FROM members WHERE id = ?", [$memberId]);
if (!$member) return 0;
// Board-offer-sourced discounts have no bonus_free_subscription_years concept.
if (empty($member['special_discount_id']) || ($member['special_discount_source'] ?? 'special_discount') !== 'special_discount') {
// Check conditional discounts
$membershipValue = $member['membership_value'] ?? '0.00';
$totalPaid = self::getTotalMembershipPaid($db, $memberId);
......@@ -90,6 +96,73 @@ final class SpecialDiscountService
return (int) $discount['bonus_free_subscription_years'];
}
/**
* Resolve the member's directly-assigned "special discount" from whichever source it came
* from — the special_discounts catalog (board-decision-backed entries only) or an active
* board_offers cash discount. Returns null if none assigned/active/in-range.
*/
public static function resolveAssignedDiscount(array $member): ?array
{
if (empty($member['special_discount_id'])) return null;
$db = App::getInstance()->db();
$today = date('Y-m-d');
$source = $member['special_discount_source'] ?? 'special_discount';
if ($source === 'board_offer') {
$offer = $db->selectOne(
"SELECT * FROM board_offers WHERE id = ? AND is_active = 1 AND cash_discount_type IS NOT NULL AND cash_discount_value IS NOT NULL",
[(int) $member['special_discount_id']]
);
if (!$offer || !self::isWithinDateRange($offer, $today)) return null;
return [
'source' => 'board_offer',
'id' => (int) $offer['id'],
'label' => 'عرض مجلس الإدارة: ' . $offer['title_ar'],
'discount_type' => $offer['cash_discount_type'],
'discount_value' => $offer['cash_discount_value'],
'requires_document' => false,
'bonus_free_subscription_years' => 0,
'applies_to' => $offer['applies_to'] ?? 'membership_fee',
'row' => $offer,
];
}
$discount = $db->selectOne(
"SELECT * FROM special_discounts WHERE id = ? AND is_active = 1",
[(int) $member['special_discount_id']]
);
if (!$discount || !self::isWithinDateRange($discount, $today)) return null;
$discType = $discount['discount_type'] ?? 'percentage';
return [
'source' => 'special_discount',
'id' => (int) $discount['id'],
'label' => 'خصم خاص: ' . $discount['name_ar'],
'discount_type' => $discType,
'discount_value' => $discType === 'fixed_amount' ? ($discount['fixed_amount'] ?? '0.00') : ($discount['discount_percentage'] ?? '0.00'),
'requires_document' => (bool) ($discount['requires_document'] ?? false),
'bonus_free_subscription_years' => (int) ($discount['bonus_free_subscription_years'] ?? 0),
'applies_to' => $discount['applies_to'] ?? 'membership_fee',
'row' => $discount,
];
}
/**
* Compute a discount amount for a normalized type ('percentage'|'fixed_amount'|'free_subscription').
*/
public static function amountForType(string $type, string $value, string $baseAmount): string
{
if ($type === 'percentage') {
return bcdiv(bcmul($baseAmount, $value, 4), '100', 2);
}
if ($type === 'fixed_amount') {
return $value;
}
return '0.00';
}
/**
* Apply free subscription bonus: mark N years of subscriptions as paid (discount = 100%).
*/
......
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE `members`
DROP FOREIGN KEY `fk_members_special_discount`;
ALTER TABLE `members`
ADD COLUMN `special_discount_source` ENUM('special_discount','board_offer') NOT NULL DEFAULT 'special_discount' AFTER `special_discount_id`;
UPDATE `members` SET `special_discount_source` = 'special_discount' WHERE `special_discount_id` IS NOT NULL",
'down' => "
ALTER TABLE `members`
DROP COLUMN `special_discount_source`;
ALTER TABLE `members`
ADD CONSTRAINT `fk_members_special_discount` FOREIGN KEY (`special_discount_id`) REFERENCES `special_discounts`(`id`) ON DELETE SET NULL",
];
......@@ -90,7 +90,8 @@ app/Modules/Members/
| gender | VARCHAR | male/female |
| qualification_id | INT FK→qualifications | Determines pricing |
| membership_value | DECIMAL | Stored for reference |
| special_discount_id | INT FK→special_discounts | Selectable only from board-approved discounts (has `board_decision_number`) — see Pricing.md |
| special_discount_id | BIGINT UNSIGNED, no FK (NEW) | Points into `special_discounts` OR `board_offers` depending on `special_discount_source`. No hard FK — a single column can't FK two tables; integrity is enforced in `MemberController::parseDiscountSelection()`. See Pricing.md |
| special_discount_source | ENUM('special_discount','board_offer') NOT NULL DEFAULT 'special_discount' (NEW) | Disambiguates which table `special_discount_id` points into. Resolve via `SpecialDiscountService::resolveAssignedDiscount($member)` rather than joining directly |
| discount_amount | DECIMAL | |
| payment_method | VARCHAR | cash/installment/check/visa/transfer |
| activated_by_payment_id | INT FK→payments | Links to activating payment |
......@@ -370,3 +371,14 @@ dropped → active (within 1 year + board approval + payment)
- reconcile() on every page view is a hidden migration — should be event-driven
- Multiple financial year calculation functions duplicated across services
- Board offer logic tightly coupled to payment request creation
## Change Log
- 2026-08-30: The member "special discount" dropdown (fill-form, edit, show/apply-discount) now merges
two sources — board-approved `special_discounts` and active `board_offers` cash discounts — instead of
only the former. `members.special_discount_id` lost its FK to `special_discounts` (migration
`Phase_106_001`) and gained a sibling `special_discount_source` column so it can point at either table.
All reads of the member's assigned discount should go through
`SpecialDiscountService::resolveAssignedDiscount($member)` rather than joining `special_discounts`
directly — `BillingService::getMemberBill()` and `MemberController::show()` were updated to do this;
check for new direct `special_discount_id` joins before adding more. See Pricing.md's Change Log for
the full history of this feature (board-decision requirement, then the source merge).
......@@ -56,6 +56,13 @@ app/Modules/Pricing/
for record-keeping or the auto-applied `condition_type` bonus flow in `SpecialDiscountService`) but cannot
be manually assigned to a member. Already-assigned discounts on existing members are unaffected regardless
of whether they have a decision number (no retroactive changes).
- **Member dropdown is now merged with board_offers (NEW, 2026-08-30)** — the member-level "special discount"
select shows TWO optgroups: `عروض مجلس الإدارة` (from `BoardOffer::allActiveWithCashDiscount()` — active
board_offers with a cash discount configured) and `الخصومات الخاصة` (from
`SpecialDiscount::allActiveBoardApproved()`). Option values are prefixed (`bo:<id>` / `sd:<id>`) so a
single `special_discount_id` form field can select from either table. See `members.special_discount_source`
in Members.md and `SpecialDiscountService::resolveAssignedDiscount()` for how the two sources are unified
server-side.
### regulatory_discounts (NEW)
- Bylaw-mandated discount rules (Articles 97-102, 110)
......@@ -158,3 +165,10 @@ Regulatory discounts are NOT stackable — system picks the highest applicable o
that the special-discount dropdown pulled from an unaudited catalog instead of board-approved
offers, and that board offers could not express a fixed-amount down payment. See
`docs/architecture-maps/Members.md` for the consuming side (`MemberController`, `show.php`).
- 2026-08-30 (follow-up): client clarified the dropdown should show board_offers discounts *in addition
to* board-approved special_discounts, not instead of them (they'd stopped seeing anything they'd added
under عروض مجلس الإدارة). Added `members.special_discount_source` (migration `Phase_106_001`) and
merged both sources into one dropdown via `BoardOffer::allActiveWithCashDiscount()` +
`SpecialDiscountService::resolveAssignedDiscount()`/`amountForType()`. Dropped the hard FK
`fk_members_special_discount` (it can no longer be enforced against a single table) — integrity is now
app-level, validated in `MemberController::parseDiscountSelection()`.
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