fix(odeme): önce kredi sonra paid damgası; çift pakette iade akışı; UNIQUE tespiti
All checks were successful
Deploy / deploy (push) Successful in 8m50s
All checks were successful
Deploy / deploy (push) Successful in 8m50s
- odemeyiSonuclandir krediyi yazıp sonra paid damgalar; araya kesinti girerse
sipariş pending kalır ve yeniden denenir (eskiden: paid görünür, kredi yok).
paid + deftersiz eski siparişler ilk ziyarette onarılır
- paket tek seferlik: grantCredits({ paketTekil }) zaten paketliye yazmaz
(kontrol UPDATE'in WHERE'inde). Sipariş paid damgalanır, senkron log +
destek@ e-postası, /odeme/sonuc 'Bu ödeme iade edilecek' kartı, /kosullar#iade
- drizzle DrizzleQueryError üst mesajında 'UNIQUE' yok → idempotent no-op yerine
hata fırlıyordu (grant + iki spend yolu); cause zincirine bakılıyor
- callback geçici DB hatasında 500 yerine sonuç sayfasına yönlendirir
- krediKaydiVarMi (reason, ref_id) indeksini kullanır; haftalık mutabakat sorgusu
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,30 @@ export const RAPOR_KREDI = 3;
|
||||
/** Paketle gelen geri bildirimli liste revizyonu hakkı */
|
||||
export const MAX_REVIZYON = 2;
|
||||
|
||||
/**
|
||||
* UNIQUE ihlali mi? drizzle sürücü hatasını DrizzleQueryError'a sarar: üst
|
||||
* mesaj "Failed query: …" olur, "UNIQUE" yalnız `cause` zincirinde kalır. Yalnız
|
||||
* üst mesaja bakmak idempotent no-op yerine hatayı yukarı fırlatıyordu.
|
||||
*/
|
||||
function uniqueIhlaliMi(err: unknown): boolean {
|
||||
for (let e: unknown = err, i = 0; e != null && i < 5; i++) {
|
||||
const h = e as {
|
||||
message?: unknown;
|
||||
code?: unknown;
|
||||
extendedCode?: unknown;
|
||||
cause?: unknown;
|
||||
};
|
||||
if (
|
||||
String(h.message ?? "").includes("UNIQUE") ||
|
||||
String(h.extendedCode ?? h.code ?? "").includes("CONSTRAINT_UNIQUE")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
e = h.cause;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export type SpendResult =
|
||||
| { ok: true }
|
||||
| { ok: false; error: "INSUFFICIENT" | "DUPLICATE" };
|
||||
@@ -61,7 +85,7 @@ export async function spendCredits(opts: {
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg === "INSUFFICIENT") return { ok: false, error: "INSUFFICIENT" };
|
||||
if (msg.includes("UNIQUE")) return { ok: false, error: "DUPLICATE" };
|
||||
if (uniqueIhlaliMi(err)) return { ok: false, error: "DUPLICATE" };
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -108,14 +132,22 @@ export async function spendCreditForMessage(opts: {
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg === "INSUFFICIENT") return { ok: false, error: "INSUFFICIENT" };
|
||||
if (msg.includes("UNIQUE")) return { ok: false, error: "DUPLICATE" };
|
||||
if (uniqueIhlaliMi(err)) return { ok: false, error: "DUPLICATE" };
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/** `paketTekil` ile çağrılan tanımlamada kullanıcı zaten paketliyse fırlatılır. */
|
||||
class ZatenPaketli extends Error {}
|
||||
|
||||
/**
|
||||
* Kredi tanımlama (deneme, satın alma, top-up, iade). UNIQUE(reason, refId)
|
||||
* sayesinde aynı referansla ikinci çağrı sessizce no-op olur → idempotent.
|
||||
*
|
||||
* `paketTekil`: paket tek seferliktir. Kullanıcı başka bir siparişle zaten
|
||||
* paketliyse kredi YAZILMAZ (`neden: "zaten-paketli"`); çağıran bu ödemeyi
|
||||
* iade edilecek diye işaretler. Kontrol, bakiye güncellemesinin WHERE'inde
|
||||
* yapılır ki eşzamanlı iki ödeme de tek paket yazabilsin.
|
||||
*/
|
||||
export async function grantCredits(opts: {
|
||||
userId: string;
|
||||
@@ -123,7 +155,8 @@ export async function grantCredits(opts: {
|
||||
reason: "trial_grant" | "purchase" | "topup" | "refund";
|
||||
refId: string;
|
||||
setHasPaket?: boolean;
|
||||
}): Promise<{ granted: boolean }> {
|
||||
paketTekil?: boolean;
|
||||
}): Promise<{ granted: boolean; neden?: "tekrar" | "zaten-paketli" }> {
|
||||
try {
|
||||
await appDb.transaction(async (tx) => {
|
||||
await tx.insert(creditLedger).values({
|
||||
@@ -134,7 +167,7 @@ export async function grantCredits(opts: {
|
||||
refId: opts.refId,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
await tx
|
||||
const res = await tx
|
||||
.update(user)
|
||||
.set({
|
||||
creditBalance: sql`${user.creditBalance} + ${opts.delta}`,
|
||||
@@ -143,16 +176,41 @@ export async function grantCredits(opts: {
|
||||
krediBittiAt: null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(user.id, opts.userId));
|
||||
.where(
|
||||
opts.paketTekil
|
||||
? and(eq(user.id, opts.userId), eq(user.hasPaket, false))
|
||||
: eq(user.id, opts.userId),
|
||||
);
|
||||
// 0 satır: kullanıcı zaten paketli → defter kaydını da geri al
|
||||
if (opts.paketTekil && res.rowsAffected === 0) throw new ZatenPaketli();
|
||||
});
|
||||
return { granted: true };
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
if (msg.includes("UNIQUE")) return { granted: false };
|
||||
if (err instanceof ZatenPaketli) {
|
||||
return { granted: false, neden: "zaten-paketli" };
|
||||
}
|
||||
if (uniqueIhlaliMi(err)) return { granted: false, neden: "tekrar" };
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bu siparişin kredisi deftere düşmüş mü? `reason` şart: UNIQUE(reason, ref_id)
|
||||
* indeksi kullanılsın (yalnız ref_id tam tablo tarar) ve aynı refId'li bir
|
||||
* `refund` satırı "kredi yazılmış" sayılmasın.
|
||||
*/
|
||||
export async function krediKaydiVarMi(
|
||||
refId: string,
|
||||
reason: "purchase" | "topup",
|
||||
): Promise<boolean> {
|
||||
const rows = await appDb
|
||||
.select({ id: creditLedger.id })
|
||||
.from(creditLedger)
|
||||
.where(and(eq(creditLedger.reason, reason), eq(creditLedger.refId, refId)))
|
||||
.limit(1);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kredi-bitti anını işaretler (48 saatlik hatırlatma e-postasının saati).
|
||||
* Yalnızca ilk bitişte yazar (kolon doluysa no-op) ve hatırlatma zaten
|
||||
|
||||
Reference in New Issue
Block a user