qube_sync 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +235 -75
- data/Rakefile +12 -0
- data/lib/qube_sync/client.rb +579 -0
- data/lib/qube_sync/operations/account.rb +337 -0
- data/lib/qube_sync/operations/account_tax_line_info.rb +30 -0
- data/lib/qube_sync/operations/aging_report.rb +160 -0
- data/lib/qube_sync/operations/alert.rb +91 -0
- data/lib/qube_sync/operations/ar_refund_credit_card.rb +399 -0
- data/lib/qube_sync/operations/bar_code.rb +37 -0
- data/lib/qube_sync/operations/bill.rb +1073 -0
- data/lib/qube_sync/operations/bill_payment_check.rb +413 -0
- data/lib/qube_sync/operations/bill_payment_credit_card.rb +315 -0
- data/lib/qube_sync/operations/bill_to_pay.rb +66 -0
- data/lib/qube_sync/operations/billing_rate.rb +172 -0
- data/lib/qube_sync/operations/budget_summary_report.rb +80 -0
- data/lib/qube_sync/operations/build_assembly.rb +318 -0
- data/lib/qube_sync/operations/charge.rb +452 -0
- data/lib/qube_sync/operations/check.rb +1055 -0
- data/lib/qube_sync/operations/class.rb +176 -0
- data/lib/qube_sync/operations/cleared_status.rb +39 -0
- data/lib/qube_sync/operations/company.rb +39 -0
- data/lib/qube_sync/operations/company_activity.rb +31 -0
- data/lib/qube_sync/operations/credit_card_charge.rb +1000 -0
- data/lib/qube_sync/operations/credit_card_credit.rb +1000 -0
- data/lib/qube_sync/operations/credit_memo.rb +1243 -0
- data/lib/qube_sync/operations/currency.rb +195 -0
- data/lib/qube_sync/operations/custom_detail_report.rb +176 -0
- data/lib/qube_sync/operations/custom_summary_report.rb +180 -0
- data/lib/qube_sync/operations/customer.rb +744 -0
- data/lib/qube_sync/operations/customer_msg.rb +117 -0
- data/lib/qube_sync/operations/customer_type.rb +133 -0
- data/lib/qube_sync/operations/data_event_recovery_info.rb +54 -0
- data/lib/qube_sync/operations/data_ext.rb +201 -0
- data/lib/qube_sync/operations/data_ext_def.rb +179 -0
- data/lib/qube_sync/operations/date_driven_terms.rb +131 -0
- data/lib/qube_sync/operations/deposit.rb +555 -0
- data/lib/qube_sync/operations/employee.rb +577 -0
- data/lib/qube_sync/operations/employee_default.rb +100 -0
- data/lib/qube_sync/operations/entity.rb +118 -0
- data/lib/qube_sync/operations/estimate.rb +1055 -0
- data/lib/qube_sync/operations/form1099_category_account_mapping.rb +90 -0
- data/lib/qube_sync/operations/general_detail_report.rb +185 -0
- data/lib/qube_sync/operations/general_summary_report.rb +180 -0
- data/lib/qube_sync/operations/host.rb +36 -0
- data/lib/qube_sync/operations/inventory_adjustment.rb +482 -0
- data/lib/qube_sync/operations/inventory_site.rb +214 -0
- data/lib/qube_sync/operations/invoice.rb +1208 -0
- data/lib/qube_sync/operations/item.rb +115 -0
- data/lib/qube_sync/operations/item_assemblies_can_build.rb +44 -0
- data/lib/qube_sync/operations/item_discount.rb +319 -0
- data/lib/qube_sync/operations/item_fixed_asset.rb +317 -0
- data/lib/qube_sync/operations/item_group.rb +253 -0
- data/lib/qube_sync/operations/item_inventory.rb +446 -0
- data/lib/qube_sync/operations/item_inventory_assembly.rb +483 -0
- data/lib/qube_sync/operations/item_non_inventory.rb +461 -0
- data/lib/qube_sync/operations/item_other_charge.rb +434 -0
- data/lib/qube_sync/operations/item_payment.rb +268 -0
- data/lib/qube_sync/operations/item_receipt.rb +1011 -0
- data/lib/qube_sync/operations/item_sales_tax.rb +270 -0
- data/lib/qube_sync/operations/item_sales_tax_group.rb +200 -0
- data/lib/qube_sync/operations/item_service.rb +455 -0
- data/lib/qube_sync/operations/item_sites.rb +96 -0
- data/lib/qube_sync/operations/item_subtotal.rb +182 -0
- data/lib/qube_sync/operations/job_report.rb +156 -0
- data/lib/qube_sync/operations/job_type.rb +131 -0
- data/lib/qube_sync/operations/journal_entry.rb +518 -0
- data/lib/qube_sync/operations/lead.rb +216 -0
- data/lib/qube_sync/operations/list.rb +42 -0
- data/lib/qube_sync/operations/list_deleted.rb +57 -0
- data/lib/qube_sync/operations/list_display.rb +59 -0
- data/lib/qube_sync/operations/list_merge.rb +55 -0
- data/lib/qube_sync/operations/other_name.rb +245 -0
- data/lib/qube_sync/operations/paycheck.rb +139 -0
- data/lib/qube_sync/operations/payment_method.rb +128 -0
- data/lib/qube_sync/operations/payroll_detail_report.rb +161 -0
- data/lib/qube_sync/operations/payroll_item.rb +75 -0
- data/lib/qube_sync/operations/payroll_item_addition.rb +98 -0
- data/lib/qube_sync/operations/payroll_item_company_contribution.rb +134 -0
- data/lib/qube_sync/operations/payroll_item_deduction.rb +124 -0
- data/lib/qube_sync/operations/payroll_item_direct_deposit.rb +70 -0
- data/lib/qube_sync/operations/payroll_item_non_wage.rb +93 -0
- data/lib/qube_sync/operations/payroll_item_tax.rb +123 -0
- data/lib/qube_sync/operations/payroll_item_wage.rb +168 -0
- data/lib/qube_sync/operations/payroll_item_workers_compensation.rb +108 -0
- data/lib/qube_sync/operations/payroll_last_period.rb +30 -0
- data/lib/qube_sync/operations/payroll_liability_adjustment.rb +204 -0
- data/lib/qube_sync/operations/payroll_liability_check.rb +137 -0
- data/lib/qube_sync/operations/payroll_prior_payment.rb +178 -0
- data/lib/qube_sync/operations/payroll_summary_report.rb +157 -0
- data/lib/qube_sync/operations/payroll_year_to_date_adjustment.rb +211 -0
- data/lib/qube_sync/operations/preferences.rb +61 -0
- data/lib/qube_sync/operations/price_level.rb +305 -0
- data/lib/qube_sync/operations/purchase_order.rb +1038 -0
- data/lib/qube_sync/operations/qb_command_exec.rb +37 -0
- data/lib/qube_sync/operations/receive_payment.rb +627 -0
- data/lib/qube_sync/operations/receive_payment_to_deposit.rb +38 -0
- data/lib/qube_sync/operations/sales_order.rb +1045 -0
- data/lib/qube_sync/operations/sales_receipt.rb +1337 -0
- data/lib/qube_sync/operations/sales_rep.rb +172 -0
- data/lib/qube_sync/operations/sales_tax_code.rb +205 -0
- data/lib/qube_sync/operations/sales_tax_payable.rb +37 -0
- data/lib/qube_sync/operations/sales_tax_payment_check.rb +375 -0
- data/lib/qube_sync/operations/sales_tax_return.rb +35 -0
- data/lib/qube_sync/operations/sales_tax_return_line.rb +43 -0
- data/lib/qube_sync/operations/ship_method.rb +116 -0
- data/lib/qube_sync/operations/special_account.rb +55 -0
- data/lib/qube_sync/operations/special_item.rb +44 -0
- data/lib/qube_sync/operations/standard_terms.rb +125 -0
- data/lib/qube_sync/operations/template.rb +36 -0
- data/lib/qube_sync/operations/terms.rb +91 -0
- data/lib/qube_sync/operations/time_report.rb +127 -0
- data/lib/qube_sync/operations/time_tracking.rb +303 -0
- data/lib/qube_sync/operations/to_do.rb +260 -0
- data/lib/qube_sync/operations/transaction.rb +356 -0
- data/lib/qube_sync/operations/transfer.rb +213 -0
- data/lib/qube_sync/operations/transfer_inventory.rb +384 -0
- data/lib/qube_sync/operations/txn.rb +38 -0
- data/lib/qube_sync/operations/txn_deleted.rb +54 -0
- data/lib/qube_sync/operations/txn_display.rb +76 -0
- data/lib/qube_sync/operations/txn_void.rb +36 -0
- data/lib/qube_sync/operations/unit_of_measure_set.rb +149 -0
- data/lib/qube_sync/operations/vehicle.rb +152 -0
- data/lib/qube_sync/operations/vehicle_mileage.rb +191 -0
- data/lib/qube_sync/operations/vendor.rb +661 -0
- data/lib/qube_sync/operations/vendor_credit.rb +1008 -0
- data/lib/qube_sync/operations/vendor_type.rb +131 -0
- data/lib/qube_sync/operations/workers_comp_code.rb +184 -0
- data/lib/qube_sync/operations.rb +1165 -0
- data/lib/qube_sync/version.rb +1 -1
- data/lib/qube_sync.rb +1 -0
- data/sig/qube_sync/client.rbs +82 -0
- data/sig/qube_sync/operations/account.rbs +65 -0
- data/sig/qube_sync/operations/account_tax_line_info.rbs +13 -0
- data/sig/qube_sync/operations/aging_report.rbs +29 -0
- data/sig/qube_sync/operations/alert.rbs +53 -0
- data/sig/qube_sync/operations/ar_refund_credit_card.rbs +53 -0
- data/sig/qube_sync/operations/bar_code.rbs +16 -0
- data/sig/qube_sync/operations/bill.rbs +78 -0
- data/sig/qube_sync/operations/bill_payment_check.rbs +62 -0
- data/sig/qube_sync/operations/bill_payment_credit_card.rbs +45 -0
- data/sig/qube_sync/operations/bill_to_pay.rbs +19 -0
- data/sig/qube_sync/operations/billing_rate.rbs +40 -0
- data/sig/qube_sync/operations/budget_summary_report.rbs +22 -0
- data/sig/qube_sync/operations/build_assembly.rbs +64 -0
- data/sig/qube_sync/operations/charge.rbs +80 -0
- data/sig/qube_sync/operations/check.rbs +81 -0
- data/sig/qube_sync/operations/class.rbs +43 -0
- data/sig/qube_sync/operations/cleared_status.rbs +16 -0
- data/sig/qube_sync/operations/company.rbs +15 -0
- data/sig/qube_sync/operations/company_activity.rbs +14 -0
- data/sig/qube_sync/operations/credit_card_charge.rbs +69 -0
- data/sig/qube_sync/operations/credit_card_credit.rbs +69 -0
- data/sig/qube_sync/operations/credit_memo.rbs +162 -0
- data/sig/qube_sync/operations/currency.rbs +52 -0
- data/sig/qube_sync/operations/custom_detail_report.rbs +31 -0
- data/sig/qube_sync/operations/custom_summary_report.rbs +33 -0
- data/sig/qube_sync/operations/customer.rbs +174 -0
- data/sig/qube_sync/operations/customer_msg.rbs +31 -0
- data/sig/qube_sync/operations/customer_type.rbs +32 -0
- data/sig/qube_sync/operations/data_event_recovery_info.rbs +20 -0
- data/sig/qube_sync/operations/data_ext.rbs +53 -0
- data/sig/qube_sync/operations/data_ext_def.rbs +51 -0
- data/sig/qube_sync/operations/date_driven_terms.rbs +35 -0
- data/sig/qube_sync/operations/deposit.rbs +100 -0
- data/sig/qube_sync/operations/employee.rbs +173 -0
- data/sig/qube_sync/operations/employee_default.rbs +68 -0
- data/sig/qube_sync/operations/entity.rbs +24 -0
- data/sig/qube_sync/operations/estimate.rbs +148 -0
- data/sig/qube_sync/operations/form1099_category_account_mapping.rbs +33 -0
- data/sig/qube_sync/operations/general_detail_report.rbs +31 -0
- data/sig/qube_sync/operations/general_summary_report.rbs +32 -0
- data/sig/qube_sync/operations/host.rbs +19 -0
- data/sig/qube_sync/operations/inventory_adjustment.rbs +105 -0
- data/sig/qube_sync/operations/inventory_site.rbs +66 -0
- data/sig/qube_sync/operations/invoice.rbs +144 -0
- data/sig/qube_sync/operations/item.rbs +25 -0
- data/sig/qube_sync/operations/item_assemblies_can_build.rbs +15 -0
- data/sig/qube_sync/operations/item_discount.rbs +62 -0
- data/sig/qube_sync/operations/item_fixed_asset.rbs +97 -0
- data/sig/qube_sync/operations/item_group.rbs +62 -0
- data/sig/qube_sync/operations/item_inventory.rbs +87 -0
- data/sig/qube_sync/operations/item_inventory_assembly.rbs +94 -0
- data/sig/qube_sync/operations/item_non_inventory.rbs +64 -0
- data/sig/qube_sync/operations/item_other_charge.rbs +59 -0
- data/sig/qube_sync/operations/item_payment.rbs +55 -0
- data/sig/qube_sync/operations/item_receipt.rbs +71 -0
- data/sig/qube_sync/operations/item_sales_tax.rbs +57 -0
- data/sig/qube_sync/operations/item_sales_tax_group.rbs +50 -0
- data/sig/qube_sync/operations/item_service.rbs +62 -0
- data/sig/qube_sync/operations/item_sites.rbs +26 -0
- data/sig/qube_sync/operations/item_subtotal.rbs +48 -0
- data/sig/qube_sync/operations/job_report.rbs +28 -0
- data/sig/qube_sync/operations/job_type.rbs +32 -0
- data/sig/qube_sync/operations/journal_entry.rbs +88 -0
- data/sig/qube_sync/operations/lead.rbs +91 -0
- data/sig/qube_sync/operations/list.rbs +15 -0
- data/sig/qube_sync/operations/list_deleted.rbs +17 -0
- data/sig/qube_sync/operations/list_display.rbs +21 -0
- data/sig/qube_sync/operations/list_merge.rbs +22 -0
- data/sig/qube_sync/operations/other_name.rbs +71 -0
- data/sig/qube_sync/operations/paycheck.rbs +27 -0
- data/sig/qube_sync/operations/payment_method.rbs +33 -0
- data/sig/qube_sync/operations/payroll_detail_report.rbs +29 -0
- data/sig/qube_sync/operations/payroll_item.rbs +23 -0
- data/sig/qube_sync/operations/payroll_item_addition.rbs +48 -0
- data/sig/qube_sync/operations/payroll_item_company_contribution.rbs +58 -0
- data/sig/qube_sync/operations/payroll_item_deduction.rbs +54 -0
- data/sig/qube_sync/operations/payroll_item_direct_deposit.rbs +30 -0
- data/sig/qube_sync/operations/payroll_item_non_wage.rbs +23 -0
- data/sig/qube_sync/operations/payroll_item_tax.rbs +58 -0
- data/sig/qube_sync/operations/payroll_item_wage.rbs +48 -0
- data/sig/qube_sync/operations/payroll_item_workers_compensation.rbs +42 -0
- data/sig/qube_sync/operations/payroll_last_period.rbs +13 -0
- data/sig/qube_sync/operations/payroll_liability_adjustment.rbs +59 -0
- data/sig/qube_sync/operations/payroll_liability_check.rbs +27 -0
- data/sig/qube_sync/operations/payroll_prior_payment.rbs +48 -0
- data/sig/qube_sync/operations/payroll_summary_report.rbs +30 -0
- data/sig/qube_sync/operations/payroll_year_to_date_adjustment.rbs +66 -0
- data/sig/qube_sync/operations/preferences.rbs +26 -0
- data/sig/qube_sync/operations/price_level.rbs +57 -0
- data/sig/qube_sync/operations/purchase_order.rbs +159 -0
- data/sig/qube_sync/operations/qb_command_exec.rbs +22 -0
- data/sig/qube_sync/operations/receive_payment.rbs +102 -0
- data/sig/qube_sync/operations/receive_payment_to_deposit.rbs +15 -0
- data/sig/qube_sync/operations/sales_order.rbs +153 -0
- data/sig/qube_sync/operations/sales_receipt.rbs +139 -0
- data/sig/qube_sync/operations/sales_rep.rbs +43 -0
- data/sig/qube_sync/operations/sales_tax_code.rbs +49 -0
- data/sig/qube_sync/operations/sales_tax_payable.rbs +16 -0
- data/sig/qube_sync/operations/sales_tax_payment_check.rbs +64 -0
- data/sig/qube_sync/operations/sales_tax_return.rbs +14 -0
- data/sig/qube_sync/operations/sales_tax_return_line.rbs +15 -0
- data/sig/qube_sync/operations/ship_method.rbs +31 -0
- data/sig/qube_sync/operations/special_account.rbs +16 -0
- data/sig/qube_sync/operations/special_item.rbs +17 -0
- data/sig/qube_sync/operations/standard_terms.rbs +34 -0
- data/sig/qube_sync/operations/template.rbs +15 -0
- data/sig/qube_sync/operations/terms.rbs +23 -0
- data/sig/qube_sync/operations/time_report.rbs +26 -0
- data/sig/qube_sync/operations/time_tracking.rbs +57 -0
- data/sig/qube_sync/operations/to_do.rbs +59 -0
- data/sig/qube_sync/operations/transaction.rbs +39 -0
- data/sig/qube_sync/operations/transfer.rbs +47 -0
- data/sig/qube_sync/operations/transfer_inventory.rbs +76 -0
- data/sig/qube_sync/operations/txn.rbs +15 -0
- data/sig/qube_sync/operations/txn_deleted.rbs +17 -0
- data/sig/qube_sync/operations/txn_display.rbs +22 -0
- data/sig/qube_sync/operations/txn_void.rbs +15 -0
- data/sig/qube_sync/operations/types.rbs +434 -0
- data/sig/qube_sync/operations/unit_of_measure_set.rbs +51 -0
- data/sig/qube_sync/operations/vehicle.rbs +43 -0
- data/sig/qube_sync/operations/vehicle_mileage.rbs +38 -0
- data/sig/qube_sync/operations/vendor.rbs +142 -0
- data/sig/qube_sync/operations/vendor_credit.rbs +70 -0
- data/sig/qube_sync/operations/vendor_type.rbs +32 -0
- data/sig/qube_sync/operations/workers_comp_code.rbs +52 -0
- data/sig/qube_sync/operations.rbs +147 -0
- metadata +255 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1eac592828778d486630d76ff7d8b43072a5006682c1b8182f1c5e13f48097f2
|
|
4
|
+
data.tar.gz: 04de6f5cc7db7be2569186733e1961f291348d96e8a23dcf832ab4f40a96d722
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1011b04d773e99fb8fa441608c11f2d37a173ded5a98a41c07b3237eaa0cc67a3ea460a5fda24b9b39e39441c4dcc2a12cbe0a8c69b2d9994f89d765117a18d5
|
|
7
|
+
data.tar.gz: 5f681be6ee755144dfae40d73d2a8d3d9eb0a43ceeea9a9c712c89e09719f9a86f52bd6e3dfd056f5a00d660d1ef518f3a22eb0a6a06bccbcd54d54c465ac9a0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
- Releases publish through RubyGems trusted publishing, from `release.yml`'s OIDC token; no
|
|
4
|
+
API key is stored.
|
|
5
|
+
|
|
6
|
+
## [0.4.0] - 2026-10-25
|
|
7
|
+
|
|
8
|
+
Adds `QubeSync::Client`, a client for the v2 API. Nothing existing changes: the v1 module
|
|
9
|
+
functions still call `/api/v1` and read `QUBE_API_KEY` and `QUBE_URL`, and are now documented as
|
|
10
|
+
legacy (no runtime warning). New code should use the client.
|
|
11
|
+
|
|
12
|
+
- `QubeSync::Client.new(api_key:, base_url: "https://qubesync.com", open_timeout: 5,
|
|
13
|
+
read_timeout: 30, max_retries: 2, total_timeout: 60, on_retry: nil)`. It takes the key as an argument and never reads the
|
|
14
|
+
environment, sends it as Basic auth, and never shows it in `inspect`. `require
|
|
15
|
+
"qube_sync/client"` loads it alone, on the standard library (Net::HTTP), without Faraday.
|
|
16
|
+
- Connections: `create_connection`, `get_connection`, `list_connections`, `update_connection`,
|
|
17
|
+
`delete_connection`, `regenerate_onboarding`, `set_password`, `get_qwc`.
|
|
18
|
+
- Runs and questions: `start_run` (its `reference` is the Idempotency-Key), `get_run`,
|
|
19
|
+
`list_runs`, `cancel_run`, `run_events`, `answer`, `search`. Methods that take a link refuse
|
|
20
|
+
one on another scheme, host or port with `ArgumentError`, before any request.
|
|
21
|
+
- QuickBooks operations: one method per operation on its entity, `client.customer.create`,
|
|
22
|
+
`client.invoice.query`, `client.txn_void.execute`... (261 operations on 124 entities;
|
|
23
|
+
QuickBooks' `class` is `client.class_`), each `(connection_id, params = {}, webhook_url:,
|
|
24
|
+
idempotency_key: nil)`. They're generated from the QuickBooks operations manifest
|
|
25
|
+
(`spec/fixtures/operations.json`, served at https://qubesync.com/libraries/operations.json)
|
|
26
|
+
by `bin/generate-operations` (`rake operations:generate`), with YARD docs listing every field
|
|
27
|
+
and RBS records for the params in `sig/qube_sync/operations/`. An entity's file loads on first
|
|
28
|
+
use. CI fails when the generated files or the vendored manifest drift.
|
|
29
|
+
- `queue(operation_id, connection_id, params = {}, webhook_url:, idempotency_key: nil, method:
|
|
30
|
+
nil, path: nil)` queues an operation by its id; `method:` and `path:` send one this version
|
|
31
|
+
doesn't know. A query string repeats an array's key and brackets nested Hashes.
|
|
32
|
+
- Requests: `queue_qbxml`, `get_request`, `request_pages`, `list_requests`, `each_request`
|
|
33
|
+
(every page), `discard_request`; and `request` for anything else. Operations and
|
|
34
|
+
`queue_qbxml` make an Idempotency-Key per call when none is given, and reuse it on that call's
|
|
35
|
+
retries.
|
|
36
|
+
- Signatures for the whole client in `sig/` (`rbs -I sig validate` runs in CI).
|
|
37
|
+
- Retries: a GET, PUT, DELETE or a request with an Idempotency-Key is retried twice (0.5 s, then
|
|
38
|
+
1 s) on a failed connection, a timeout, or a 429, 502, 503 or 504, honouring `Retry-After` up
|
|
39
|
+
to 30 s. `answer` is retried too when its link has a `prompt_token`. Anything else outside 2xx
|
|
40
|
+
raises `QubeSync::APIError` (a `QubeSync::Error`) with `status`, `errors`, `code`, `message`,
|
|
41
|
+
`body` and `request_id`.
|
|
42
|
+
- A call returns or raises within `total_timeout` (60 s), retries and their waits included: each
|
|
43
|
+
attempt's timeouts are cut to the time left, and no retry is made unless a second is left after
|
|
44
|
+
its wait.
|
|
45
|
+
- `on_retry:` is called before each retry's wait with a frozen Hash (`attempt`, `max_retries`,
|
|
46
|
+
`delay`, `elapsed`, `method`, `url` without its query, `status`, `error`); an exception it
|
|
47
|
+
raises stops the call.
|
|
48
|
+
- A retried DELETE that gets a 404 after an earlier attempt's answer was lost (a read timeout, a
|
|
49
|
+
reset or closed connection, a 502 or 504) returns nil, as a 204 would.
|
|
50
|
+
|
|
3
51
|
## [0.3.0] - 2026-10-23
|
|
4
52
|
|
|
5
53
|
Adds `QubeSync::Webhooks`, which verifies a delivery and parses it into a typed event. Nothing
|
data/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# QUBESync
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Ruby library for the [QuBe Sync](https://qubesync.com) API: a client for the v2 API, and
|
|
4
|
+
webhook verification. The API's docs are at https://docs.qubesync.com.
|
|
4
5
|
|
|
5
6
|
## Installation
|
|
6
7
|
|
|
@@ -14,14 +15,245 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
|
14
15
|
|
|
15
16
|
$ gem install qube_sync
|
|
16
17
|
|
|
17
|
-
##
|
|
18
|
+
## The v2 client
|
|
19
|
+
|
|
20
|
+
`QubeSync::Client` calls the v2 API. Make one from your app's configuration: it never reads the
|
|
21
|
+
environment itself, so one app can hold a sandbox and a production client at once.
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "qube_sync" # or require "qube_sync/client" alone: it uses only the standard library
|
|
25
|
+
|
|
26
|
+
QUBE = QubeSync::Client.new(api_key: ENV.fetch("QUBE_API_KEY"))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`base_url:` (default `"https://qubesync.com"`), `open_timeout:` (5 s), `read_timeout:` (30 s),
|
|
30
|
+
`max_retries:` (2), `total_timeout:` (60 s) and `on_retry:` are options (see Retries below). Every method returns the response's `data`, a plain Hash or Array
|
|
31
|
+
with String keys, and nil for an empty answer.
|
|
32
|
+
|
|
33
|
+
### Onboarding a customer
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
connection = QUBE.create_connection(webhook_url: "https://myapp.example/qube/connections", name: "Acme Ltd")
|
|
37
|
+
customer.update!(qube_connection_id: connection["id"])
|
|
38
|
+
|
|
39
|
+
redirect_to connection["links"]["onboarding"], allow_other_host: true
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
QuBe Sync sends `connection.connected` to the connection's `webhook_url` once the customer's Web
|
|
43
|
+
Connector first checks in. `regenerate_onboarding(id)` makes a fresh onboarding link, and
|
|
44
|
+
`update_connection(id, webhook_url:, name:)` changes either. `get_connection`,
|
|
45
|
+
`list_connections`, `delete_connection`, `set_password(id, password: nil)` and `get_qwc(id)` (the
|
|
46
|
+
QWC file's XML) are there for setup without hosted onboarding.
|
|
47
|
+
|
|
48
|
+
### Starting a run and answering its question
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
run = QUBE.start_run(
|
|
52
|
+
connection_id, "sync_customer", {"customer" => {"name" => "Acme Ltd"}},
|
|
53
|
+
reference: "customer-#{customer.id}", # also sent as the Idempotency-Key
|
|
54
|
+
webhook_url: "https://myapp.example/qube/runs"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Later, a workflow_run webhook says the run is awaiting input. Its links are the run's own:
|
|
58
|
+
event = QubeSync::Webhooks.verify(raw_body, header, secrets: ENV.fetch("QUBE_WEBHOOK_SECRET"))
|
|
59
|
+
if event.awaiting_input?
|
|
60
|
+
matches = QUBE.search(event.links["search"], field: "customer", q: "Acme") # a record field's choices
|
|
61
|
+
QUBE.answer(event.links["decisions"], choice: "rename", data: {"name" => "Acme Ltd (2)"})
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`get_run(run_url)` or `get_run(connection_id, run_id)` reads a run, `list_runs(connection_id,
|
|
66
|
+
reference:, state:, outcome:)` finds runs, `cancel_run(cancel_url, reason: nil)` cancels one, and
|
|
67
|
+
`run_events(connection_id, run_id, after:, limit:)` returns a page of its event log whole
|
|
68
|
+
(`{"data" => [...], "next_after" => ...}`). A method that takes a link takes it from a webhook's
|
|
69
|
+
or a run's `links`, or a path under `/api/v2`. A link on any other scheme, host or port raises
|
|
70
|
+
`ArgumentError` before a request is made, so the API key only ever goes to QuBe Sync.
|
|
71
|
+
|
|
72
|
+
### Queueing an operation
|
|
73
|
+
|
|
74
|
+
Every QuickBooks operation is a method on its entity: `client.<entity>.<action>`, where the
|
|
75
|
+
action is `create`, `update`, `query`, `delete` or `execute`.
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
hook = "https://myapp.example/qube/requests"
|
|
79
|
+
|
|
80
|
+
request = QUBE.customer.create(connection_id, {
|
|
81
|
+
name: "Acme Ltd",
|
|
82
|
+
bill_address: {addr1: "1 Main St", city: "Austin", state: "TX"}
|
|
83
|
+
}, webhook_url: hook)
|
|
84
|
+
|
|
85
|
+
# A query sends its filters as the query string; nested filters are fine.
|
|
86
|
+
QUBE.invoice.query(connection_id, {
|
|
87
|
+
modified_date_range_filter: {from_modified_date: "2026-10-01T00:00:00"},
|
|
88
|
+
include_line_items: true,
|
|
89
|
+
iterator: true, max_returned: 50 # pages of 50, each its own webhook
|
|
90
|
+
}, webhook_url: hook)
|
|
91
|
+
|
|
92
|
+
QUBE.txn_void.execute(connection_id, {txn_void_type: "Invoice", txn_id: "1A2-1700000000"}, webhook_url: hook)
|
|
93
|
+
|
|
94
|
+
# qbXML as written.
|
|
95
|
+
QUBE.queue_qbxml(connection_id, request_xml, webhook_url: hook)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- Each method is `(connection_id, params = {}, webhook_url:, idempotency_key: nil)` and returns
|
|
99
|
+
the queued request. Its documentation (YARD, in `lib/qube_sync/operations/<entity>.rb`) lists
|
|
100
|
+
every field, nested ones included, says whether QuickBooks answers in pages, and links the API
|
|
101
|
+
reference. The params' types are RBS records in `sig/qube_sync/operations/`, for Steep or an
|
|
102
|
+
editor that reads RBS.
|
|
103
|
+
- An entity's file loads the first time you use it, so `require "qube_sync/client"` stays quick.
|
|
104
|
+
- QuickBooks' `class` entity is `client.class_` (`client.class` is Ruby's).
|
|
105
|
+
- `queue(operation_id, connection_id, params = {}, webhook_url:, idempotency_key: nil)` queues one
|
|
106
|
+
by its id: `QUBE.queue("customer.create", connection_id, {name: "Acme Ltd"}, webhook_url: hook)`
|
|
107
|
+
sends the same request as `QUBE.customer.create`. For an operation newer than your version of
|
|
108
|
+
the gem, pass `method:` and `path:` (relative to the connection):
|
|
109
|
+
`QUBE.queue("widget.create", connection_id, params, webhook_url: hook, method: :post, path: "widgets")`.
|
|
110
|
+
- A query string repeats an array's key (`list_id=1&list_id=2`) and brackets nested Hashes
|
|
111
|
+
(`name_range_filter[from_name]=A`, `currency_filter[list_id][]=1`), as the API reads them.
|
|
112
|
+
|
|
113
|
+
The answer comes to `webhook_url` (see Webhooks below). `get_request(connection_id, id)`,
|
|
114
|
+
`request_pages(connection_id, id)` and `discard_request(connection_id, id)` act on one request.
|
|
115
|
+
`list_requests(connection_id, page:, page_size:, state:, ...)` returns a page whole (`{"data" =>
|
|
116
|
+
[...], "meta" => {"page" => 1, "total_pages" => 3}}`), and `each_request(connection_id, **filters)`
|
|
117
|
+
yields every request on every page (an Enumerator without a block).
|
|
118
|
+
|
|
119
|
+
`request(method, path_or_url, body = nil, query: {}, headers: {})` calls anything else and returns
|
|
120
|
+
the whole parsed body.
|
|
121
|
+
|
|
122
|
+
### Retries, idempotency and errors
|
|
123
|
+
|
|
124
|
+
- A GET, PUT or DELETE, or any request with an `Idempotency-Key`, is retried up to `max_retries`
|
|
125
|
+
times (after 0.5 s, then 1 s) when the connection fails or times out, or QuBe Sync answers 429,
|
|
126
|
+
502, 503 or 504. A `Retry-After` on a 429 or 503 is honoured, up to 30 s. Other requests
|
|
127
|
+
(`create_connection`, `cancel_run`...) are sent once.
|
|
128
|
+
- A call returns or raises within `total_timeout` (60 s), retries and their waits included: each
|
|
129
|
+
attempt's timeouts are cut to the time left, and a retry is made only when at least a second
|
|
130
|
+
would be left after its wait (a `Retry-After` that doesn't fit isn't waited, nor shortened; the
|
|
131
|
+
call raises what the last attempt got). Pass `max_retries: 0` for calls made while a user waits
|
|
132
|
+
on a page, if 60 s is too long.
|
|
133
|
+
- `answer` is retried when its link has a `prompt_token`, as a run's `links.decisions` always
|
|
134
|
+
does while it asks. A retry after a lost answer may raise a 409 with code `prompt_changed`:
|
|
135
|
+
the question has been answered, by the first attempt or by someone else. Without a
|
|
136
|
+
`prompt_token` it's sent once, since an answer sent again could be taken for the run's next
|
|
137
|
+
question.
|
|
138
|
+
- A retried DELETE (`delete_connection`, or `request(:delete, ...)`) that gets a 404 returns nil,
|
|
139
|
+
as a 204 would, when an earlier attempt's answer was lost after the request could have reached
|
|
140
|
+
QuBe Sync (a read timeout, a connection reset or closed, a 502 or 504): that attempt deleted it.
|
|
141
|
+
Any other 404 raises, including a retry's after a refused connection, a connect timeout, a 429
|
|
142
|
+
or a 503.
|
|
143
|
+
- `on_retry:` takes a callable, called before each retry's wait with a frozen Hash: `attempt`
|
|
144
|
+
(1, 2...), `max_retries`, `delay` and `elapsed` (seconds), `method`, `url` (without its query
|
|
145
|
+
string), `status` (for a status retry) and `error` (for a network failure), and never the key,
|
|
146
|
+
headers or body. It isn't called when no retry is made. An exception it raises stops the call
|
|
147
|
+
and reaches the caller.
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
QubeSync::Client.new(api_key: ENV.fetch("QUBE_API_KEY"),
|
|
151
|
+
on_retry: ->(r) { Rails.logger.warn("QuBe Sync retry #{r[:attempt]}/#{r[:max_retries]} #{r[:method]} #{r[:url]} #{r[:status] || r[:error].class}") })
|
|
152
|
+
```
|
|
153
|
+
- `start_run` sends `reference` as the Idempotency-Key. `queue` and `queue_qbxml` take
|
|
154
|
+
`idempotency_key:`; without one the client makes a UUID per call and sends it again on that
|
|
155
|
+
call's retries, so a retry never queues a request twice.
|
|
156
|
+
- Any other answer outside 2xx raises `QubeSync::APIError` (a `QubeSync::Error`): `status`,
|
|
157
|
+
`errors` (the body's `errors` as parsed), `code` (`errors["code"]`, such as `prompt_changed`, or
|
|
158
|
+
nil), `message` (one line, safe to log), `body` and `request_id` (the `x-request-id` header, for
|
|
159
|
+
support). A connection that still fails after the retries raises Ruby's own error
|
|
160
|
+
(`Net::OpenTimeout`, `Net::ReadTimeout`, `Errno::ECONNREFUSED`...).
|
|
161
|
+
- The API key never appears in an error or in `inspect`.
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
begin
|
|
165
|
+
QUBE.answer(decisions_url, choice: "rename")
|
|
166
|
+
rescue QubeSync::APIError => e
|
|
167
|
+
raise unless e.code == "prompt_changed" # the question changed: show the new one
|
|
168
|
+
refresh_question(run)
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Webhooks
|
|
173
|
+
|
|
174
|
+
`QubeSync::Webhooks` verifies a delivery's `X-QUBE-Signature` header and parses the body into a
|
|
175
|
+
typed event. It doesn't need Rails or Rack, and never reads the environment: pass it the raw body,
|
|
176
|
+
the header and your webhook secret.
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
require "qube_sync"
|
|
180
|
+
|
|
181
|
+
# raw_body: the request body exactly as received, before anything parses it.
|
|
182
|
+
# header: the X-QUBE-Signature header (nil when it's absent).
|
|
183
|
+
def handle_qube_webhook(raw_body, header)
|
|
184
|
+
event = QubeSync::Webhooks.verify(raw_body, header, secrets: ENV.fetch("QUBE_WEBHOOK_SECRET"))
|
|
185
|
+
|
|
186
|
+
case event.type
|
|
187
|
+
when "request"
|
|
188
|
+
# A queued request's answer. Deduplicate on event.dedupe_key ("<page_request_id>:<state>").
|
|
189
|
+
# event.response_json, event.error, event.page, event.iteration_state
|
|
190
|
+
when "workflow_run"
|
|
191
|
+
# A run is waiting for an answer, or has ended. Skip it unless event.newer_than?(stored)
|
|
192
|
+
# for the revision stored with event.reference: deliveries can arrive out of order.
|
|
193
|
+
# event.awaiting_input? / completed? / failed? / cancelled?, event.prompt, event.output
|
|
194
|
+
when "notification"
|
|
195
|
+
# Data a running workflow sent. Deduplicate on event.dedupe_key (the delivery_id), and
|
|
196
|
+
# never skip one by revision. event.name, event.data
|
|
197
|
+
when "connection"
|
|
198
|
+
# Connected, setup cancelled, stalled or resumed. Deduplicate on event.dedupe_key, and skip
|
|
199
|
+
# it unless event.newer_than?(the revision stored for event.connection_id).
|
|
200
|
+
when "name_index"
|
|
201
|
+
# event.connection_id, event.name_index
|
|
202
|
+
else
|
|
203
|
+
# An event this version doesn't know yet: acknowledge it.
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
200
|
|
207
|
+
rescue QubeSync::Webhooks::SignatureError => e
|
|
208
|
+
# e.reason is :missing, :malformed, :stale or :mismatch. The message is safe to log.
|
|
209
|
+
warn "QuBe Sync webhook refused: #{e.message}"
|
|
210
|
+
401
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
- `secrets:` takes one secret or a list. While a secret rotates, a delivery carries one `v1`
|
|
215
|
+
signature per active secret, and either secret verifies it.
|
|
216
|
+
- The timestamp must be within 300 seconds of now, either way. `tolerance:` (seconds) sets
|
|
217
|
+
another window, and `now:` (unix seconds or a `Time`) sets the clock, for tests.
|
|
218
|
+
- Every event has `type`, `event`, `timestamp`, `app_id`, `sandbox`, `dedupe_key` and `to_h`
|
|
219
|
+
(the whole body). A missing key reads as `nil`, and `event["any_key"]` reads any other key.
|
|
220
|
+
- `QubeSync::Webhooks.parse(raw_body)` builds the event without verifying it, for tests or for
|
|
221
|
+
code that verified the delivery elsewhere.
|
|
222
|
+
|
|
223
|
+
In Rails, read the raw body with `request.raw_post`:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
class QubeWebhooksController < ActionController::API
|
|
227
|
+
def create
|
|
228
|
+
event = QubeSync::Webhooks.verify(
|
|
229
|
+
request.raw_post,
|
|
230
|
+
request.headers[QubeSync::Webhooks::HEADER],
|
|
231
|
+
secrets: Rails.application.credentials.qube_webhook_secret
|
|
232
|
+
)
|
|
233
|
+
QubeWebhookJob.perform_later(event.to_h) # route by type in the job, as above
|
|
234
|
+
head :ok
|
|
235
|
+
rescue QubeSync::Webhooks::SignatureError => e
|
|
236
|
+
Rails.logger.warn("QuBe Sync webhook refused: #{e.message}")
|
|
237
|
+
head :unauthorized
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`QubeSync.verify_and_build_webhook!` (0.2.4) still works, and still returns the parsed Hash.
|
|
243
|
+
|
|
244
|
+
## v1 (legacy)
|
|
245
|
+
|
|
246
|
+
The v1 module functions below call `/api/v1` and still work, but the v2 client above is the one to
|
|
247
|
+
use for new code.
|
|
248
|
+
|
|
249
|
+
### Configuration
|
|
18
250
|
|
|
19
251
|
You'll need to set two evironment variables in your application to use this gem:
|
|
20
252
|
|
|
21
253
|
`QUBE_API_KEY` - This is your API key, which you can find in your QUBE Sync application's settings.
|
|
22
254
|
`QUBE_WEBHOOK_SECRET` - This is the secret key used to sign the webhook payloads. You can find this in your QUBE Sync application's settings.
|
|
23
255
|
|
|
24
|
-
|
|
256
|
+
### Usage
|
|
25
257
|
|
|
26
258
|
```ruby
|
|
27
259
|
require 'qube_sync'
|
|
@@ -128,78 +360,6 @@ QubeSync.verify_and_build_webhook!(request.body.read, request.headers['X-Qube-Si
|
|
|
128
360
|
QubeSync.verify_and_build_webhook!(request.body.read, request.headers['X-Qube-Signature'], max_age: 120)
|
|
129
361
|
```
|
|
130
362
|
|
|
131
|
-
## Webhooks
|
|
132
|
-
|
|
133
|
-
`QubeSync::Webhooks` verifies a delivery's `X-QUBE-Signature` header and parses the body into a
|
|
134
|
-
typed event. It doesn't need Rails or Rack, and never reads the environment: pass it the raw body,
|
|
135
|
-
the header and your webhook secret.
|
|
136
|
-
|
|
137
|
-
```ruby
|
|
138
|
-
require "qube_sync"
|
|
139
|
-
|
|
140
|
-
# raw_body: the request body exactly as received, before anything parses it.
|
|
141
|
-
# header: the X-QUBE-Signature header (nil when it's absent).
|
|
142
|
-
def handle_qube_webhook(raw_body, header)
|
|
143
|
-
event = QubeSync::Webhooks.verify(raw_body, header, secrets: ENV.fetch("QUBE_WEBHOOK_SECRET"))
|
|
144
|
-
|
|
145
|
-
case event.type
|
|
146
|
-
when "request"
|
|
147
|
-
# A queued request's answer. Deduplicate on event.dedupe_key ("<page_request_id>:<state>").
|
|
148
|
-
# event.response_json, event.error, event.page, event.iteration_state
|
|
149
|
-
when "workflow_run"
|
|
150
|
-
# A run is waiting for an answer, or has ended. Skip it unless event.newer_than?(stored)
|
|
151
|
-
# for the revision stored with event.reference: deliveries can arrive out of order.
|
|
152
|
-
# event.awaiting_input? / completed? / failed? / cancelled?, event.prompt, event.output
|
|
153
|
-
when "notification"
|
|
154
|
-
# Data a running workflow sent. Deduplicate on event.dedupe_key (the delivery_id), and
|
|
155
|
-
# never skip one by revision. event.name, event.data
|
|
156
|
-
when "connection"
|
|
157
|
-
# Connected, setup cancelled, stalled or resumed. Deduplicate on event.dedupe_key, and skip
|
|
158
|
-
# it unless event.newer_than?(the revision stored for event.connection_id).
|
|
159
|
-
when "name_index"
|
|
160
|
-
# event.connection_id, event.name_index
|
|
161
|
-
else
|
|
162
|
-
# An event this version doesn't know yet: acknowledge it.
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
200
|
|
166
|
-
rescue QubeSync::Webhooks::SignatureError => e
|
|
167
|
-
# e.reason is :missing, :malformed, :stale or :mismatch. The message is safe to log.
|
|
168
|
-
warn "QuBe Sync webhook refused: #{e.message}"
|
|
169
|
-
401
|
|
170
|
-
end
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
- `secrets:` takes one secret or a list. While a secret rotates, a delivery carries one `v1`
|
|
174
|
-
signature per active secret, and either secret verifies it.
|
|
175
|
-
- The timestamp must be within 300 seconds of now, either way. `tolerance:` (seconds) sets
|
|
176
|
-
another window, and `now:` (unix seconds or a `Time`) sets the clock, for tests.
|
|
177
|
-
- Every event has `type`, `event`, `timestamp`, `app_id`, `sandbox`, `dedupe_key` and `to_h`
|
|
178
|
-
(the whole body). A missing key reads as `nil`, and `event["any_key"]` reads any other key.
|
|
179
|
-
- `QubeSync::Webhooks.parse(raw_body)` builds the event without verifying it, for tests or for
|
|
180
|
-
code that verified the delivery elsewhere.
|
|
181
|
-
|
|
182
|
-
In Rails, read the raw body with `request.raw_post`:
|
|
183
|
-
|
|
184
|
-
```ruby
|
|
185
|
-
class QubeWebhooksController < ActionController::API
|
|
186
|
-
def create
|
|
187
|
-
event = QubeSync::Webhooks.verify(
|
|
188
|
-
request.raw_post,
|
|
189
|
-
request.headers[QubeSync::Webhooks::HEADER],
|
|
190
|
-
secrets: Rails.application.credentials.qube_webhook_secret
|
|
191
|
-
)
|
|
192
|
-
QubeWebhookJob.perform_later(event.to_h) # route by type in the job, as above
|
|
193
|
-
head :ok
|
|
194
|
-
rescue QubeSync::Webhooks::SignatureError => e
|
|
195
|
-
Rails.logger.warn("QuBe Sync webhook refused: #{e.message}")
|
|
196
|
-
head :unauthorized
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
`QubeSync.verify_and_build_webhook!` (0.2.4) still works, and still returns the parsed Hash.
|
|
202
|
-
|
|
203
363
|
## Development
|
|
204
364
|
|
|
205
365
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/Rakefile
CHANGED
|
@@ -6,3 +6,15 @@ require "rspec/core/rake_task"
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
8
|
task default: :spec
|
|
9
|
+
|
|
10
|
+
namespace :operations do
|
|
11
|
+
desc "Write the per-operation methods and signatures from spec/fixtures/operations.json"
|
|
12
|
+
task :generate do
|
|
13
|
+
ruby "bin/generate-operations"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "Fail when the generated operations are stale, missing or orphaned"
|
|
17
|
+
task :check do
|
|
18
|
+
ruby "bin/generate-operations", "--check"
|
|
19
|
+
end
|
|
20
|
+
end
|