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

חשבונית אוטומטית מ-PayPlus: מה-callback למסמך

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

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

מגדירים כתובת callback

ב-PayPlus אפשר להגדיר את הכתובת ברמת המסוף (הגדרות מערכת, ניהול מסופי כרטיסי אשראי, הגדרות מסוף), ברמת דף התשלום, או לכל בקשה בנפרד:

PaymentPages/generateLink
{
  "payment_page_uid": "...",
  "amount": 472,
  "currency_code": "ILS",
  "more_info": "1001",
  "refURL_callback": "https://your-server.example.com/hooks/payplus"
}

בכל עסקה שחויבה בהצלחה PayPlus שולחת לכתובת הזאת את פרטי העסקה כ-JSON.

מאמתים את כותרת ה-hash

callback של PayPlus מגיע עם שתי כותרות: user-agent: PayPlus, ו-hash. ה-hash הוא HMAC-SHA256 של גוף הבקשה, עם המפתח הסודי של ה-API שלכם, בקידוד base64. מחשבים אותו על הבייטים כמו שהגיעו.

payplus.mjsGitHub
// Two headers mark a callback: `user-agent: PayPlus` and `hash`, the base64 HMAC-SHA256 of the
// body keyed with your API secret key. It is computed here on the raw bytes as they arrived.
export function verify({ headers, rawBody, secret }) {
  if (headers['user-agent'] !== 'PayPlus') return false
  const sent = Buffer.from(String(headers.hash || ''), 'base64')
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest()
  return sent.length === expected.length && crypto.timingSafeEqual(sent, expected)
}

מה-callback לתשלום

שדהמה יש בו
transaction_typeCharge לחיוב.
transaction.status_code000 לעסקה מאושרת.
transaction.uidמזהה העסקה. זה מפתח ה-idempotency.
transaction.amountהסכום, כמספר עשרוני.
transaction.more_infoמספר ההזמנה שלכם, כפי ששלחתם.
data.itemsהפריטים: שם, כמות, והסכום ששולם על כל אחד.
invoiceמופיע כש-PayPlus הפיקה מסמך לעסקה.
payplus.mjsGitHub
// transaction.amount is a decimal number in transaction.currency; status_code "000" is approved.
export function parse({ rawBody }) {
  const c = JSON.parse(rawBody)
  const t = c.transaction
  if (c.transaction_type !== 'Charge' || t?.status_code !== '000') return null
  // PayPlus can issue the document itself. When it did, the callback says so, and a second
  // one from here would be a second tax document for the same sale.
  if (c.invoice?.docu_number) return null
  const items = (c.data?.items || []).map((i) => ({
    description: i.quantity > 1 ? `${i.name} × ${i.quantity}` : i.name,
    quantity: 1,
    unitAmount: toMinor(i.amount_pay),
  }))
  return {
    payment: {
      source: name,
      id: t.uid,
      amount: toMinor(t.amount),
      currency: t.currency || 'ILS',
      method: 'card',
      // more_info is your own reference, as you sent it when the page was created.
      description: `הזמנה ${t.more_info || t.number}`,
      customer: { name: c.data?.card_information?.card_holder_name },
      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
}

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

כדאי לדעת

  • מסמך אחד לעסקה. אם הפקת חשבוניות פעילה אצלכם ב-PayPlus, ה-callback יכלול את המסמך שהופק שם, והקוד ידלג. בוחרים מקום אחד שמפיק.
  • שם הלקוח. ה-callback נושא את שם בעל הכרטיס. אימייל וטלפון כדאי לקחת מההזמנה אצלכם, לפי more_info.
  • עסקאות שנכשלו. אפשר לבקש callback גם עליהן. הקוד מפיק רק על חיוב מאושר.
  • זיכוי. מבצעים אותו בקריאה Transactions/RefundByTransactionUID, ומפיקים את חשבונית הזיכוי באותו מקום בקוד, עם מזהה הזיכוי כמפתח idempotency.

שאלות נפוצות

איפה מגדירים את כתובת ה-callback ב-PayPlus?

בשלושה מקומות אפשריים: בהגדרות המסוף, בהגדרות דף התשלום, או לכל בקשה בנפרד בשדה refURL_callback של הקריאה PaymentPages/generateLink.

איך מאמתים callback של PayPlus?

בודקים שתי כותרות: user-agent שערכה PayPlus, ו-hash, שהיא HMAC-SHA256 של גוף הבקשה עם המפתח הסודי של ה-API שלכם, בקידוד base64.

PayPlus יכולה להפיק חשבונית בעצמה. למה להפיק ב-BillOS?

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

איך מקשרים עסקה להזמנה שלי?

בשדה more_info שולחים את מספר ההזמנה שלכם כשיוצרים את דף התשלום. הוא חוזר ב-callback.

אפשר לבדוק עסקה גם בלי לחכות ל-callback?

כן. הקריאה PaymentPages/ipn מחזירה את מצב העסקה לפי המזהה שלה. זה שימושי כבדיקה תקופתית שמוודאת שלכל עסקה הופק מסמך.

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

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

מפתח Sandbox בחינם