Talep ile Aktarım
Yoklayıcı Örnek Kodları
Node.js, C# ve Python ile yoklama döngüsü: GET, claim, complete, fail.
Son güncelleme: 21 Eylül 2026 · Sözleşme sürümü 1.0.0
Üç dilde aynı döngü: GET → claim → complete {veri} ya da fail (ekstrede complete'ten önce statement/rows parçaları). Anahtar MAI_API_KEY ortam değişkeninden okunur; kodda tutmayın. Önce test ortamında mai_test_ anahtarıyla çalıştırın; talebi panelden bırakıp kartın "Geldi" demesini bekleyin.
Node.js — yoklama döngüsü (dört işlem: GET · claim · complete{veri} · fail)▾
const BASE = "https://mutabakat.ai/api/v1";
const H = { Authorization: `Bearer ${process.env.MAI_API_KEY}`, "Content-Type": "application/json" };
const api = (method, path, body, idem) =>
fetch(BASE + path, { method, headers: { ...H, ...(idem ? { "Idempotency-Key": idem } : {}) }, body: body ? JSON.stringify(body) : undefined })
.then(async (r) => ({ status: r.status, retryAfter: Number(r.headers.get("retry-after") ?? 0), body: await r.json().catch(() => null) }));
const transient = (status) => status === 429 || status >= 500; // geçici: talebi KAPATMAYIN, sonraki turda yeniden deneyin
// ERP'den veriyi hazırla → tek çağrıyla teslim et; dönem, cari ve başlık talepten türetilir.
async function fulfil(req) {
const claim = await api("POST", `/erp-requests/${req.id}/claim`); // "aldım, hazırlıyorum" (isteğe bağlı)
if (claim.status !== 200) { console.warn(req.id, "claim", claim.status, claim.body?.error?.code, "— atlandı"); return; } // 409: başka program aldı / süresi doldu; 429/5xx: sonraki tur
let body;
try {
if (req.kind === "statement") {
const { counterparty, period_start, period_end } = req.params; // { external_id, name, tax_number }
const rows = await erp.lineItems(counterparty.external_id, period_start, period_end); // [{ date, description, reference_no, amount | debit/credit }]
// Ekstre satırları PARÇA PARÇA gider (≤20.000 satır/parça, toplam ≤250.000); sonra complete bitirir.
const PART = 20000, total_parts = Math.max(1, Math.ceil(rows.length / PART));
for (let part = 1; part <= total_parts; part++) {
const chunk = await api("POST", `/erp-requests/${req.id}/statement/rows`, { part, total_parts, rows: rows.slice((part - 1) * PART, part * PART) }, `part:${req.id}:${part}`); // Idempotency-Key parça başına
if (chunk.status !== 200) {
if (transient(chunk.status)) console.warn(req.id, "parça", part, "geçici hata", chunk.status, "→ sonraki turda aynı parçadan devam"); // rezervasyon pending'e döner; fail ÇAĞIRMAYIN
else console.error(req.id, "parça", part, "kalıcı ret", chunk.status, chunk.body?.error); // 400/413/422: sunucu talebi failed yaptı
return;
}
}
body = { statement: { total_parts } };
} else if (req.kind === "balances") {
const items = await erp.openBalances(req.params.period_date);
body = { balances: { records: items.map((b) => ({ counterparty: { external_id: b.code, name: b.name, tax_number: b.taxNo }, amount: Math.abs(b.balance).toFixed(2), direction: b.balance >= 0 ? "debit" : "credit" })) } };
} else {
body = { counterparties: await erp.allVendors() }; // [{ external_id, name, tax_number, email }] ≤ 10.000
}
} catch (err) {
// Veri ÜRETİLEMEDİ (cari yok, ERP kapalı…): kalıcı iş hatası — talebi kapatın, sebep panelde görünür.
await api("POST", `/erp-requests/${req.id}/fail`, { reason: String(err.message).slice(0, 300) });
return;
}
const res = await api("POST", `/erp-requests/${req.id}/complete`, body, `complete:${req.id}`); // Idempotency-Key: talep başına SABİT
if (res.status === 200) { console.log(req.kind, "teslim edildi", res.body.delivery); return; } // { row_count | campaign_id | accepted… }
if (transient(res.status)) { console.warn(req.id, "geçici hata", res.status, "→ sonraki turda yeniden (Retry-After:", res.retryAfter, "sn)"); return; } // rezervasyon pending'e döner; fail ÇAĞIRMAYIN
console.error(req.id, "kalıcı ret", res.status, res.body?.error); // 400/402/413/422: sunucu talebi zaten failed yaptı; aynı veriyi yeniden göndermeyin
}
async function loop() {
for (;;) {
let wait = 20;
try {
const { status, body, retryAfter } = await api("GET", "/erp-requests");
if (status === 200) {
if (body.fetch_mode !== "request") console.warn("Firma 'Talep ile aktarım' seçmemiş; talep gelmeyecek:", body.fetch_mode);
for (const req of body.data) await fulfil(req).catch((err) => console.error(req.id, "ağ hatası; sonraki turda yeniden denenecek:", err.message));
wait = body.poll_after_seconds ?? 20;
} else if (status === 429) wait = retryAfter || 60;
} catch (err) { console.error("yoklama başarısız (ağ?):", err.message); } // döngü ölmez
await new Promise((r) => setTimeout(r, wait * 1000));
}
}
loop();C# — yoklama döngüsü (Windows servisi / zamanlanmış görev)▾
var http = new HttpClient { BaseAddress = new Uri("https://mutabakat.ai/api/v1/") };
http.DefaultRequestHeaders.Authorization = new("Bearer", Environment.GetEnvironmentVariable("MAI_API_KEY"));
static bool Transient(HttpStatusCode s) => (int)s == 429 || (int)s >= 500; // geçici: talebi KAPATMAYIN, sonraki turda yeniden deneyin
async Task Fulfil(ErpRequest req)
{
var claim = await http.PostAsync($"erp-requests/{req.Id}/claim", null); // "aldım, hazırlıyorum" (isteğe bağlı)
if (!claim.IsSuccessStatusCode) { Log.Warn($"{req.Id} claim {(int)claim.StatusCode} — atlandı"); return; } // 409 başka program aldı; 429/5xx sonraki tur
object body;
try
{
if (req.Kind == "statement")
{
// Ekstre satırları PARÇA PARÇA gider (≤20.000 satır/parça, toplam ≤250.000); sonra complete bitirir.
var rows = Erp.LineItems(req.Params.Counterparty.ExternalId, req.Params.PeriodStart, req.Params.PeriodEnd);
const int Part = 20000; var totalParts = Math.Max(1, (rows.Count + Part - 1) / Part);
for (var part = 1; part <= totalParts; part++)
{
var chunk = new HttpRequestMessage(HttpMethod.Post, $"erp-requests/{req.Id}/statement/rows") { Content = JsonContent.Create(new { part, total_parts = totalParts, rows = rows.Skip((part - 1) * Part).Take(Part) }) };
chunk.Headers.Add("Idempotency-Key", $"part:{req.Id}:{part}"); // parça başına
var sent = await http.SendAsync(chunk);
if (!sent.IsSuccessStatusCode) { Log.Warn($"{req.Id} parça {part} {(int)sent.StatusCode} — {(Transient(sent.StatusCode) ? "sonraki turda aynı parçadan devam" : "kalıcı ret, talep failed")}"); return; }
}
body = new { statement = new { total_parts = totalParts } };
}
else
{
body = req.Kind switch
{
"balances" => new { balances = new { records = Erp.OpenBalances(req.Params.PeriodDate) } },
_ => new { counterparties = Erp.AllVendors() },
};
}
}
catch (Exception ex)
{
// Veri ÜRETİLEMEDİ (ERP kapalı, cari yok): kalıcı iş hatası — talebi kapatın, sebep panelde görünür.
await http.PostAsJsonAsync($"erp-requests/{req.Id}/fail", new { reason = ex.Message[..Math.Min(300, ex.Message.Length)] });
return;
}
var deliver = new HttpRequestMessage(HttpMethod.Post, $"erp-requests/{req.Id}/complete") { Content = JsonContent.Create(body) };
deliver.Headers.Add("Idempotency-Key", $"complete:{req.Id}"); // zorunlu; talep başına SABİT (tekrar deneme ikinci kez işlenmez)
var res = await http.SendAsync(deliver);
if (res.IsSuccessStatusCode) { Log.Info($"{req.Kind} teslim edildi"); return; } // 200 → { data, delivery: { row_count | campaign_id | accepted… } }
if (Transient(res.StatusCode)) { Log.Warn($"{req.Id} geçici hata {(int)res.StatusCode}; sonraki turda yeniden (Retry-After: {res.Headers.RetryAfter})"); return; } // rezervasyon pending'e döner; fail ÇAĞIRMAYIN
Log.Error($"{req.Id} kalıcı ret {(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}"); // 400/402/413/422: sunucu talebi failed yaptı; aynı veriyi yeniden göndermeyin
}
while (true)
{
var wait = 20;
try
{
var list = await http.GetFromJsonAsync<ErpRequestList>("erp-requests");
if (list!.FetchMode != "request") Log.Warn($"Firma 'Talep ile aktarım' seçmemiş ({list.FetchMode}); talep gelmeyecek.");
foreach (var req in list.Data)
{
try { await Fulfil(req); }
catch (HttpRequestException ex) { Log.Warn($"{req.Id} ağ hatası; sonraki turda yeniden: {ex.Message}"); }
}
wait = list.PollAfterSeconds;
}
catch (Exception ex) { Log.Error($"yoklama başarısız (ağ?): {ex.Message}"); } // döngü ölmez
await Task.Delay(TimeSpan.FromSeconds(wait));
}Python — yoklama döngüsü▾
import os, time, requests
BASE = "https://mutabakat.ai/api/v1"
H = {"Authorization": f"Bearer {os.environ['MAI_API_KEY']}"}
def api(method, path, json=None, idem=None):
headers = {**H, **({"Idempotency-Key": idem} if idem else {})}
return requests.request(method, BASE + path, json=json, headers=headers, timeout=120)
def transient(status): # geçici: talebi KAPATMAYIN, sonraki turda yeniden deneyin
return status == 429 or status >= 500
def fulfil(req):
claim = api("POST", f"/erp-requests/{req['id']}/claim") # "aldım, hazırlıyorum" (isteğe bağlı)
if claim.status_code != 200:
print(req["id"], "claim", claim.status_code, "— atlandı") # 409 başka program aldı; 429/5xx sonraki tur
return
try:
p = req["params"]
if req["kind"] == "statement":
# Ekstre satırları PARÇA PARÇA gider (≤20.000 satır/parça, toplam ≤250.000); sonra complete bitirir.
rows = erp.line_items(p["counterparty"]["external_id"], p["period_start"], p["period_end"])
PART = 20000
total_parts = max(1, -(-len(rows) // PART))
for part in range(1, total_parts + 1):
chunk = api("POST", f"/erp-requests/{req['id']}/statement/rows", {"part": part, "total_parts": total_parts, "rows": rows[(part - 1) * PART: part * PART]}, f"part:{req['id']}:{part}") # Idempotency-Key parça başına
if chunk.status_code != 200:
print(req["id"], "parça", part, chunk.status_code, "— sonraki turda aynı parçadan devam" if transient(chunk.status_code) else "— kalıcı ret, talep failed")
return
body = {"statement": {"total_parts": total_parts}}
elif req["kind"] == "balances":
body = {"balances": {"records": erp.open_balances(p["period_date"])}}
else:
body = {"counterparties": erp.all_vendors()}
except Exception as exc:
# Veri ÜRETİLEMEDİ (ERP kapalı, cari yok): kalıcı iş hatası — talebi kapatın, sebep panelde görünür.
api("POST", f"/erp-requests/{req['id']}/fail", {"reason": str(exc)[:300]})
return
r = api("POST", f"/erp-requests/{req['id']}/complete", body, f"complete:{req['id']}") # Idempotency-Key talep başına SABİT
if r.status_code == 200:
print(req["kind"], "teslim edildi", r.json()["delivery"]) # {"row_count" | "campaign_id" | "accepted"...}
elif transient(r.status_code):
print(req["id"], "geçici hata", r.status_code, "→ sonraki turda yeniden (Retry-After:", r.headers.get("Retry-After"), ")") # fail ÇAĞIRMAYIN
else:
print(req["id"], "kalıcı ret", r.status_code, r.text[:300]) # sunucu talebi failed yaptı; aynı veriyi yeniden göndermeyin
while True:
wait = 20
try:
res = api("GET", "/erp-requests")
if res.status_code == 200:
body = res.json()
if body.get("fetch_mode") != "request":
print("Firma 'Talep ile aktarım' seçmemiş; talep gelmeyecek:", body.get("fetch_mode"))
for req in body.get("data", []):
try:
fulfil(req)
except requests.RequestException as exc:
print(req["id"], "ağ hatası; sonraki turda yeniden:", exc)
wait = body.get("poll_after_seconds", 20)
elif res.status_code == 429:
wait = int(res.headers.get("Retry-After", 60))
except requests.RequestException as exc:
print("yoklama başarısız (ağ?):", exc) # döngü ölmez
time.sleep(wait)