למפתחים · מתשלום למסמך

חשבונית אוטומטית מקארדקום, על כל עסקה בדף התשלום

דף התשלום של קארדקום (Low Profile) שולח לשרת שלכם את תוצאת העסקה. מהתוצאה הזאת אפשר להפיק חשבונית מס קבלה ב-BillOS, בספר אחד עם שאר המסמכים של העסק. כאן החיבור המלא ל-API בגרסה 11.

עודכן ב- · 4 דקות קריאה · נכתב על ידי בר אסולין ובר אביזמר, מייסדי BillOS

מעבירים WebHookUrl ביצירת דף התשלום

בקריאה LowProfile/Create יש שני שדות שהחיבור הזה נשען עליהם:

שדהמה שמים בו
WebHookUrlהכתובת בשרת שלכם שתקבל את תוצאת העסקה.
ReturnValueמספר ההזמנה שלכם. הוא חוזר בהודעה, וכך מקשרים עסקה להזמנה.

ההודעה לא חתומה, ולכן הכתובת נושאת סוד:

WebHookUrl
https://your-server.example.com/hooks/cardcom?key=LONG_RANDOM_SECRET

מפיקים ממה שקארדקום עונה, לא ממה שנשלח

כל אחד שמכיר את הכתובת יכול לשלוח אליה JSON שנראה כמו עסקה מאושרת. לכן הגוף של ההודעה משמש רק כדי לדעת איזו עסקה לבדוק. את התוצאה עצמה קוראים מקארדקום, ב-LowProfile/GetLpResult, ומה שחוזר משם הוא מה שהופך למסמך.

cardcom.mjsGitHub
// The webhook carries no signature, so its body is treated as a hint and the result is read
// from CardCom itself: LowProfile/GetLpResult answers the same object for a LowProfileId.
// What this returns is the body the rest of the handler trusts.
export async function verify({ query, rawBody, secret }) {
  if (!sameSecret(query.get('key'), secret)) return false
  const { LowProfileId } = JSON.parse(rawBody)
  const res = await fetch(`${process.env.CARDCOM_API_URL || 'https://secure.cardcom.solutions'}/api/v11/LowProfile/GetLpResult`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ TerminalNumber: Number(process.env.CARDCOM_TERMINAL), ApiName: process.env.CARDCOM_API_NAME, LowProfileId }),
  })
  return res.ok ? await res.text() : false
}

מהתוצאה לתשלום

שדהמה יש בו
ResponseCode0 לעסקה שהצליחה.
TranzactionIdמספר העסקה. זה מפתח ה-idempotency.
ReturnValueמספר ההזמנה שלכם, כפי ששלחתם.
TranzactionInfo.Amountהסכום, כמספר עשרוני.
TranzactionInfo.CoinId1 שקל, 2 דולר.
UIValuesמה שהלקוח מילא: שם בעל הכרטיס, אימייל וטלפון.
DocumentInfoמופיע כשקארדקום הפיקה מסמך לעסקה.
cardcom.mjsGitHub
// ResponseCode 0 is a successful deal. Amount is a decimal in the deal's coin.
const COINS = { 1: 'ILS', 2: 'USD' }

export function parse({ rawBody }) {
  const r = JSON.parse(rawBody)
  const t = r.TranzactionInfo
  if (r.ResponseCode !== 0 || !t || t.IsRefund) return null
  // CardCom can issue the document itself (the Document block of LowProfile/Create). When it
  // did, a second one from here would be a second tax document for the same sale.
  if (r.DocumentInfo?.DocumentNumber) return null
  return {
    payment: {
      source: name,
      id: String(r.TranzactionId),
      amount: toMinor(t.Amount),
      currency: COINS[t.CoinId] || 'ILS',
      method: 'card',
      // ReturnValue is your own order id, as you sent it to LowProfile/Create.
      description: `הזמנה ${r.ReturnValue || r.TranzactionId}`,
      customer: {
        name: r.UIValues?.CardOwnerName || t.CardOwnerName,
        email: r.UIValues?.CardOwnerEmail || t.CardOwnerEmail,
        phone: r.UIValues?.CardOwnerPhone || t.CardOwnerPhone,
      },
      items: [],
    },
  }
}

מהתשלום לחשבונית

מכאן הקוד זהה לכל מקור תשלום.

billos.mjsGitHub
export async function issueForPayment(payment, { apiKey, businessId }) {
  const { business } = await call('GET', `/businesses/${businessId}`, { apiKey })
  // The money is already in, so the document carries the sale and its payment together:
  // a tax invoice/receipt (320). An exempt dealer (osek patur) may not issue a tax invoice,
  // and gives a receipt (400) instead.
  const docType = business.dealerType === 'patur' ? 400 : 320

  // The items are printed only when they add up to what was charged. Shipping, a coupon or a
  // rounding rule on the shop's side would otherwise make a document that disagrees with the
  // payment, and the payment is the fact.
  const itemsTotal = payment.items.reduce((sum, i) => sum + Math.round(i.quantity * i.unitAmount), 0)
  const lines = payment.items.length && itemsTotal === payment.amount
    ? payment.items.map((i) => ({ description: i.description, quantity: i.quantity, unitPriceExVat: i.unitAmount }))
    : [{ description: payment.description, quantity: 1, unitPriceExVat: payment.amount }]

  const { name, email, phone, taxId } = payment.customer
  const { document } = await call('POST', `/businesses/${businessId}/documents`, {
    apiKey,
    // The source's own payment id. A notification delivered twice lands on the same document.
    idempotencyKey: `${payment.source}:${payment.id}`,
    body: {
      docType,
      // The customer was charged a VAT-inclusive price. In gross mode unitPriceExVat carries
      // that price, the total is exactly what was paid, and the VAT is derived from it.
      priceMode: 'gross',
      ...(payment.currency === 'ILS' ? {} : { currency: payment.currency }),
      party: { name: name || email || 'לקוח', ...(email ? { email } : {}), ...(phone ? { phone } : {}), ...(taxId ? { taxId } : {}) },
      lines,
      payments: [{ method: payment.method, amount: payment.amount }],
    },
  })
  // A retry of a notification that was already handled: the document exists and is final.
  if (document.status === 'issued') return document
  // Issuing gives the document its number and signs the PDF. From here it never changes.
  const issued = await call('POST', `/businesses/${businessId}/documents/${document.id}/issue`, { apiKey })
  return issued.document
}

ההסבר על כל החלטה בפונקציה הזאת נמצא בעמוד מתשלום למסמך.

כדאי לדעת

  • מסמך אחד לעסקה. אם שלחתם לקארדקום בקשה להפיק מסמך יחד עם העסקה, התוצאה תכלול אותו והקוד ידלג. בוחרים מקום אחד שמפיק.
  • עונים 200. זו התשובה שקארדקום מצפה לה כדי לדעת שההודעה התקבלה.
  • לא סומכים על דף ההצלחה. הלקוח יכול לא להגיע אליו. ההודעה לשרת היא המקור.
  • זיכוי. מבצעים אותו בקריאה Transactions/RefundByTransactionId, ומפיקים את חשבונית הזיכוי באותו מקום בקוד, עם מזהה עסקת הזיכוי כמפתח idempotency.
  • הממשק הישן. בממשק הוותיק של קארדקום ההודעה נשלחת לכתובת IndicatorUrl, בבקשת GET. הקוד כאן הוא לגרסה 11.

שאלות נפוצות

איך מקבלים הודעה מקארדקום על עסקה?

בקריאה LowProfile/Create מעבירים את השדה WebHookUrl. אחרי התשלום קארדקום שולחת לכתובת הזאת את תוצאת העסקה. בשדה ReturnValue מעבירים את מספר ההזמנה שלכם, והוא חוזר בהודעה.

איך יודעים שההודעה באמת מקארדקום?

ההודעה לא חתומה. לכן מתייחסים לגוף שלה כרמז בלבד, קוראים את התוצאה מקארדקום עצמה בקריאה LowProfile/GetLpResult, ומפיקים את המסמך ממה שחזר משם.

קארדקום יכולה להפיק חשבונית בעצמה. למה להפיק ב-BillOS?

מי שמנהל את הספרים ב-BillOS רוצה את כל המסמכים בספר אחד, עם מספור אחד ועם אותם ייצואים לרואה החשבון. מה שחשוב הוא שלא יופקו שני מסמכים לאותה עסקה, והקוד בודק את זה.

פרטי הלקוח מגיעים בהודעה?

כן, מה שהלקוח מילא בדף התשלום: שם בעל הכרטיס, אימייל וטלפון.

מה עושים בזיכוי?

זיכוי בקארדקום הוא קריאת API שאתם מבצעים, Transactions/RefundByTransactionId. את חשבונית הזיכוי ב-BillOS מפיקים באותו מקום בקוד.

המסמך הראשון, עוד היום

מפתח Sandbox בחינם, בלי כרטיס אשראי. הקוד שעובד מול הבדיקות עובד מול live בלי שינוי.

מפתח Sandbox בחינם