Add SMS templates, prepaid IRT billing, and advertising shortcode 9000590009.

Charge ceil(len/64)*400 IRT per recipient across OTP, customer, and partner SMS; seed new businesses with 500k IRT; default partner sends to the advertising line.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-08 16:24:15 +03:30
co-authored by Cursor
parent 4623061a4f
commit 4a1d60dbef
29 changed files with 1170 additions and 61 deletions
+59 -1
View File
@@ -21,7 +21,7 @@ export type SmsSendOptions = {
};
/** Default Meshkee advertising shortcode when a business has no own SMS number. */
export const DEFAULT_ADVERTISING_SMS_SOURCE = '5000990009';
export const DEFAULT_ADVERTISING_SMS_SOURCE = '9000590009';
/** Gama batch size limit is undocumented; keep chunks conservative. */
const GAMA_BULK_CHUNK_SIZE = 100;
@@ -58,6 +58,64 @@ export class SmsService {
);
}
/** Meshkee service shortcode (OTP + verified SMS templates). */
getServiceSource(): string {
return (
this.config.get<string>('SMS_GAMA_SOURCE_SERVICE')?.trim() ||
'5000110005'
);
}
/**
* Best-effort account credit from Gama. Returns null when the provider
* does not expose credit over REST (check the Gama panel instead).
*/
async getAccountCredit(): Promise<{ balance: number; currency?: string } | null> {
if (!this.isEnabled() || this.isProxyConfigured()) {
return null;
}
const username = this.config.get<string>('SMS_GAMA_USERNAME')?.trim();
const password = this.config.get<string>('SMS_GAMA_PASSWORD')?.trim();
const baseUrl = (
this.config.get<string>('SMS_GAMA_BASE_URL') ?? 'https://sms.igama.ir/api/v1'
).replace(/\/$/, '');
if (!username || !password) return null;
// Gama REST docs do not document a credit endpoint; try common paths quietly.
for (const path of ['/account/credit', '/credit', '/user/credit']) {
try {
const response = await fetch(`${baseUrl}${path}`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
signal: AbortSignal.timeout(8_000),
});
if (!response.ok) continue;
const payload = (await response.json()) as {
success?: boolean;
body?: number | { credit?: number; balance?: number };
};
if (!payload.success) continue;
const raw =
typeof payload.body === 'number'
? payload.body
: payload.body?.credit ?? payload.body?.balance;
if (typeof raw === 'number' && Number.isFinite(raw)) {
return { balance: raw };
}
} catch {
// ignore and try next path
}
}
return null;
}
async sendVerificationCode(
cellNumber: string,
code: string,