feat(orders): send the order-placement SMS and close Phase 1
The reference texted three audiences on every placed order
(henza_server Api/ApiMobileController::setOrder:733-766). None of it was
wired here — OrdersService had no SmsService at all, so a customer could
place an order and nobody was told: not the customer, not the admins, not
the referring sales expert.
Now, after the transaction commits (so a rolled-back order texts nobody)
and without ever being awaited (D20 — a dead gateway must not fail an
order):
- every mobile in ORDER_ADMIN_MOBILES, with the customer's name and
the rounded gold weight;
- the customer, with distinct text for a normal and an over-limit
suspended order;
- the referring sales expert, looked up on AdminUser (the target's
referring expert is an admin, not a customer as in the reference).
The reference's four hardcoded personal numbers stay out of git, behind
the same env-var pattern as PROFILE_ADMIN_MOBILES.
The «در سیستم ویرایش شد» variant was deliberately not ported: the target
has no order-edit route, so it would be dead code — «عمداً تکرار نشد».
Also fills §2 of the admin matrix, the D1 deliverable that had been
(pending) since the phase opened: 163 rid-guarded routes, 68 distinct
rids, 176 grantable tree keys, and a per-module map to the reference
controllers. Only two enforced rids are ungrantable from the permission
screen (newsletters, persons.list.sellers.view) — filed as PERM-02,
awaiting an owner ruling because it changes the permission vocabulary.
Phase 1 (admin parity) is at its gate. api: 283/283 tests, 26 suites.
پنل: پیامک ثبت سفارش برای مشتری، ادمینها و کارشناس فروش فعال شد.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
759625d2a1
commit
83e5d2868b
@@ -6,6 +6,7 @@ import { LedgerService } from '../accounting/ledger.service';
|
||||
import { AccountingHooksService } from '../accounting-sync/accounting-hooks.service';
|
||||
import { CreateOrderDto } from './dto/create-order.dto';
|
||||
import { WageService } from '../pricing/wage.service';
|
||||
import { SmsService } from '../sms/sms.service';
|
||||
|
||||
interface ComputedItem {
|
||||
productVariantId: string;
|
||||
@@ -27,6 +28,7 @@ export class OrdersService {
|
||||
private readonly ledger: LedgerService,
|
||||
private readonly accounting: AccountingHooksService,
|
||||
private readonly wage: WageService,
|
||||
private readonly sms: SmsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -122,7 +124,7 @@ export class OrdersService {
|
||||
.gt(customer.goldCreditLimitGrams);
|
||||
const initialStatus = isSuspendedOverCreditLimit ? OrderStatus.SUSPENDED : OrderStatus.PLACED;
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const placed = await this.prisma.$transaction(async (tx) => {
|
||||
const order = await tx.order.create({
|
||||
data: {
|
||||
orderNumber: this.generateOrderNumber(),
|
||||
@@ -162,6 +164,87 @@ export class OrdersService {
|
||||
|
||||
return order;
|
||||
});
|
||||
|
||||
// Legacy sent these after the order was committed
|
||||
// (ApiMobileController.php:733-766), so a rolled-back order never texts
|
||||
// anyone. Fire-and-forget per D20.
|
||||
this.notifyOrderPlaced(placed.orderNumber, customer, totalGoldWeightGrams, isSuspendedOverCreditLimit);
|
||||
|
||||
return placed;
|
||||
}
|
||||
|
||||
/**
|
||||
* The admins who are told a new order was placed.
|
||||
*
|
||||
* Legacy hardcoded four numbers (ApiMobileController.php:740-743); this reads
|
||||
* a comma-separated env var, as the complete-profile notifier already does.
|
||||
* Unset means nobody is notified and the order itself still goes through.
|
||||
*/
|
||||
private adminMobiles(): string[] {
|
||||
return (process.env.ORDER_ADMIN_MOBILES ?? '')
|
||||
.split(',')
|
||||
.map((m) => m.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy ApiMobileController::setOrder:733-766, brand changed to منتزر.
|
||||
*
|
||||
* Three audiences: the admins, the customer, and the customer's sales expert.
|
||||
* Legacy's fourth message — «در سیستم ویرایش شد» (:734-737) — is
|
||||
* «عمداً تکرار نشد»: no target path lets a customer edit a placed order, so
|
||||
* that branch could never fire.
|
||||
*
|
||||
* `Notify::store` sat beside each of these in legacy (Firebase push); that is
|
||||
* PUSH-01, deferred to the PWA port.
|
||||
*/
|
||||
private notifyOrderPlaced(
|
||||
orderNumber: string,
|
||||
customer: { mobile: string; firstName: string | null; lastName: string | null; referredByAgentId: string | null },
|
||||
totalGoldWeightGrams: Prisma.Decimal,
|
||||
isSuspendedOverCreditLimit: boolean,
|
||||
) {
|
||||
const fire = (to: string, body: string) => {
|
||||
void this.sms.send(to, body).catch((e: unknown) => {
|
||||
this.logger.error(`Order SMS to ${to} failed: ${String(e)}`);
|
||||
});
|
||||
};
|
||||
|
||||
const name = `${customer.firstName ?? ''} ${customer.lastName ?? ''}`.trim();
|
||||
// Legacy printed Utility::round($allGram) — two decimals, trailing zeros
|
||||
// dropped, which is what Decimal's own string form gives.
|
||||
const weight = totalGoldWeightGrams.toDecimalPlaces(2).toString();
|
||||
|
||||
const adminMessage = `منتزر\nسفارشی توسط ${name}\n به وزن ${weight}\nدر سیستم ثبت شد.`;
|
||||
for (const admin of this.adminMobiles()) fire(admin, adminMessage);
|
||||
|
||||
fire(
|
||||
customer.mobile,
|
||||
isSuspendedOverCreditLimit
|
||||
? `سفارش شما به شماره ${orderNumber} به ثبت رسید اما به دلیل عبور از سقف مجاز گرمی در حالت معلق قرار گرفت. منتزر`
|
||||
: `سفارش شما به شماره ${orderNumber} به ثبت رسید. منتزر`,
|
||||
);
|
||||
|
||||
if (customer.referredByAgentId) {
|
||||
void this.notifySalesAgent(customer.referredByAgentId, name, orderNumber, weight);
|
||||
}
|
||||
}
|
||||
|
||||
/** Legacy :762-765 — the referring expert is texted too. */
|
||||
private async notifySalesAgent(agentId: string, customerName: string, orderNumber: string, weight: string) {
|
||||
try {
|
||||
const agent = await this.prisma.adminUser.findUnique({
|
||||
where: { id: agentId },
|
||||
select: { mobile: true },
|
||||
});
|
||||
if (!agent?.mobile) return;
|
||||
await this.sms.send(
|
||||
agent.mobile,
|
||||
`منتزر\nسفارشی توسط ${customerName} به شماره ${orderNumber}و به وزن ${weight} به ثبت رسید.`,
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
this.logger.error(`Order SMS to sales agent ${agentId} failed: ${String(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async updateStatus(orderId: string, newStatus: OrderStatus, actorAdminUserId?: string, note?: string) {
|
||||
|
||||
Reference in New Issue
Block a user