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

חשבונית אוטומטית מ-Grow (משולם), כולל תשלומי Bit

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

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

שתי הודעות, אותו רעיון

מאיפה התשלוםאיך מקבלים הודעה
דף תשלום, קישור תשלום, הוראת קבעעדכון שרת (Webhooks). התמיכה של Grow מפעילה אותו לחשבון, ומוסרים לה את הכתובת.
תהליך תשלום שנוצר ב-APIהשדה notifyUrl בקריאה createPaymentProcess.

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

notifyUrl
https://your-server.example.com/hooks/grow?key=LONG_RANDOM_SECRET

מאמתים בלי חתימה

הסוד שבכתובת הוא הבדיקה. Grow מפרסמת גם את רשימת כתובות ה-IP שמהן היא שולחת, ואפשר להגביל את הנתיב אליהן בחומת האש.

grow.mjsGitHub
// Grow signs nothing. The address it is given carries the secret:
//   https://shop.example.com/hooks/grow?key=<GROW_WEBHOOK_SECRET>
// Grow also publishes the addresses it sends from, for a firewall rule in front of this.
export function verify({ query, rawBody, secret }) {
  return sameSecret(query.get('key') || formFields(rawBody).webhookKey, secret)
}

מההודעה לתשלום

הגוף הוא טופס, לא JSON. שתי ההודעות קוראות לאותם דברים בשמות קצת שונים:

מהעדכון שרת של דףnotifyUrl
מזהה העסקהtransactionCodetransactionId
הסכום, בשקליםpaymentSumsum
אמצעי התשלוםtransactionType: אשראי, Bit, PayBox, העברה בנקאיתtransactionTypeId: 1 כרטיס, 6 Bit, 5 PayBox, 15 העברה
האם שולםההודעה נשלחת על תשלוםstatusCode: 2 שולם
הלקוחfullName, payerEmail, payerPhone
grow.mjsGitHub
// The body is form-encoded. A page's update names the transaction `transactionCode` and the
// amount `paymentSum`; a createPaymentProcess callback names them `transactionId` and `sum`,
// with statusCode 2 for a paid transaction. Amounts are decimal shekels.
const METHODS = { Bit: 'bit', 6: 'bit', PayBox: 'paybox', 5: 'paybox', 'העברה בנקאית': 'transfer', 15: 'transfer' }

export function parse({ rawBody }) {
  const f = formFields(rawBody)
  const id = f.transactionCode || f.transactionId
  const sum = f.paymentSum || f.sum
  if (!id || !sum) return null
  if (f.statusCode && f.statusCode !== '2') return null // 6 is a refund, 0 and 9 were not paid
  return {
    payment: {
      source: name,
      id,
      amount: toMinor(sum),
      currency: 'ILS',
      method: METHODS[f.transactionType] || METHODS[f.transactionTypeId] || 'card',
      description: f.paymentDesc || f.description || `עסקה ${f.asmachta || id}`,
      customer: { name: f.fullName, email: f.payerEmail, phone: f.payerPhone },
      items: [],
    },
  }
}

מהתשלום למסמך

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

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. מפתח ה-idempotency הוא מזהה העסקה, כך שהודעה שנשלחת שוב לא מפיקה מסמך שני.
  • אישור קבלה. בתהליך שנוצר ב-API, שולחים ל-Grow את approveTransaction אחרי שההודעה התקבלה.
  • אם Grow מפיקה אצלכם חשבוניות. בוחרים מקום אחד שמפיק. שני מסמכים לאותו תשלום הם שתי הכנסות בספרים.
  • החזר. מבצעים אותו בקריאה refundTransaction, ומפיקים את מסמך הזיכוי באותו מקום בקוד, עם מזהה ההחזר כמפתח idempotency.

שאלות נפוצות

איך מקבלים הודעה מ-Grow על תשלום?

לדפי תשלום ולקישורי תשלום יש עדכון שרת (Webhooks), שהתמיכה של Grow מפעילה לחשבון. למי שיוצר תהליך תשלום ב-API יש שדה notifyUrl בקריאה createPaymentProcess.

איך מאמתים הודעה של Grow?

Grow לא חותמת על ההודעה. שמים סוד בכתובת שמוסרים ל-Grow, כך שרק מי שמכיר את הכתובת המלאה יכול לשלוח אליה, ואפשר להוסיף כלל חומת אש לפי רשימת כתובות ה-IP ש-Grow מפרסמת.

אני עוסק פטור שגובה ב-Bit. מה מופק?

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

מה זה approveTransaction?

בתהליך תשלום שנוצר ב-API, Grow מבקשת שאחרי קבלת ההודעה תשלחו את הקריאה approveTransaction כאישור שקיבלתם אותה. לפי התיעוד של Grow העסקה מעובדת גם בלעדיה.

מה עושים בהחזר?

החזר ב-Grow הוא קריאת API שאתם מבצעים, refundTransaction. את מסמך הזיכוי ב-BillOS מפיקים באותו מקום בקוד.

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

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

מפתח Sandbox בחינם