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.
Files changed (260) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/README.md +235 -75
  4. data/Rakefile +12 -0
  5. data/lib/qube_sync/client.rb +579 -0
  6. data/lib/qube_sync/operations/account.rb +337 -0
  7. data/lib/qube_sync/operations/account_tax_line_info.rb +30 -0
  8. data/lib/qube_sync/operations/aging_report.rb +160 -0
  9. data/lib/qube_sync/operations/alert.rb +91 -0
  10. data/lib/qube_sync/operations/ar_refund_credit_card.rb +399 -0
  11. data/lib/qube_sync/operations/bar_code.rb +37 -0
  12. data/lib/qube_sync/operations/bill.rb +1073 -0
  13. data/lib/qube_sync/operations/bill_payment_check.rb +413 -0
  14. data/lib/qube_sync/operations/bill_payment_credit_card.rb +315 -0
  15. data/lib/qube_sync/operations/bill_to_pay.rb +66 -0
  16. data/lib/qube_sync/operations/billing_rate.rb +172 -0
  17. data/lib/qube_sync/operations/budget_summary_report.rb +80 -0
  18. data/lib/qube_sync/operations/build_assembly.rb +318 -0
  19. data/lib/qube_sync/operations/charge.rb +452 -0
  20. data/lib/qube_sync/operations/check.rb +1055 -0
  21. data/lib/qube_sync/operations/class.rb +176 -0
  22. data/lib/qube_sync/operations/cleared_status.rb +39 -0
  23. data/lib/qube_sync/operations/company.rb +39 -0
  24. data/lib/qube_sync/operations/company_activity.rb +31 -0
  25. data/lib/qube_sync/operations/credit_card_charge.rb +1000 -0
  26. data/lib/qube_sync/operations/credit_card_credit.rb +1000 -0
  27. data/lib/qube_sync/operations/credit_memo.rb +1243 -0
  28. data/lib/qube_sync/operations/currency.rb +195 -0
  29. data/lib/qube_sync/operations/custom_detail_report.rb +176 -0
  30. data/lib/qube_sync/operations/custom_summary_report.rb +180 -0
  31. data/lib/qube_sync/operations/customer.rb +744 -0
  32. data/lib/qube_sync/operations/customer_msg.rb +117 -0
  33. data/lib/qube_sync/operations/customer_type.rb +133 -0
  34. data/lib/qube_sync/operations/data_event_recovery_info.rb +54 -0
  35. data/lib/qube_sync/operations/data_ext.rb +201 -0
  36. data/lib/qube_sync/operations/data_ext_def.rb +179 -0
  37. data/lib/qube_sync/operations/date_driven_terms.rb +131 -0
  38. data/lib/qube_sync/operations/deposit.rb +555 -0
  39. data/lib/qube_sync/operations/employee.rb +577 -0
  40. data/lib/qube_sync/operations/employee_default.rb +100 -0
  41. data/lib/qube_sync/operations/entity.rb +118 -0
  42. data/lib/qube_sync/operations/estimate.rb +1055 -0
  43. data/lib/qube_sync/operations/form1099_category_account_mapping.rb +90 -0
  44. data/lib/qube_sync/operations/general_detail_report.rb +185 -0
  45. data/lib/qube_sync/operations/general_summary_report.rb +180 -0
  46. data/lib/qube_sync/operations/host.rb +36 -0
  47. data/lib/qube_sync/operations/inventory_adjustment.rb +482 -0
  48. data/lib/qube_sync/operations/inventory_site.rb +214 -0
  49. data/lib/qube_sync/operations/invoice.rb +1208 -0
  50. data/lib/qube_sync/operations/item.rb +115 -0
  51. data/lib/qube_sync/operations/item_assemblies_can_build.rb +44 -0
  52. data/lib/qube_sync/operations/item_discount.rb +319 -0
  53. data/lib/qube_sync/operations/item_fixed_asset.rb +317 -0
  54. data/lib/qube_sync/operations/item_group.rb +253 -0
  55. data/lib/qube_sync/operations/item_inventory.rb +446 -0
  56. data/lib/qube_sync/operations/item_inventory_assembly.rb +483 -0
  57. data/lib/qube_sync/operations/item_non_inventory.rb +461 -0
  58. data/lib/qube_sync/operations/item_other_charge.rb +434 -0
  59. data/lib/qube_sync/operations/item_payment.rb +268 -0
  60. data/lib/qube_sync/operations/item_receipt.rb +1011 -0
  61. data/lib/qube_sync/operations/item_sales_tax.rb +270 -0
  62. data/lib/qube_sync/operations/item_sales_tax_group.rb +200 -0
  63. data/lib/qube_sync/operations/item_service.rb +455 -0
  64. data/lib/qube_sync/operations/item_sites.rb +96 -0
  65. data/lib/qube_sync/operations/item_subtotal.rb +182 -0
  66. data/lib/qube_sync/operations/job_report.rb +156 -0
  67. data/lib/qube_sync/operations/job_type.rb +131 -0
  68. data/lib/qube_sync/operations/journal_entry.rb +518 -0
  69. data/lib/qube_sync/operations/lead.rb +216 -0
  70. data/lib/qube_sync/operations/list.rb +42 -0
  71. data/lib/qube_sync/operations/list_deleted.rb +57 -0
  72. data/lib/qube_sync/operations/list_display.rb +59 -0
  73. data/lib/qube_sync/operations/list_merge.rb +55 -0
  74. data/lib/qube_sync/operations/other_name.rb +245 -0
  75. data/lib/qube_sync/operations/paycheck.rb +139 -0
  76. data/lib/qube_sync/operations/payment_method.rb +128 -0
  77. data/lib/qube_sync/operations/payroll_detail_report.rb +161 -0
  78. data/lib/qube_sync/operations/payroll_item.rb +75 -0
  79. data/lib/qube_sync/operations/payroll_item_addition.rb +98 -0
  80. data/lib/qube_sync/operations/payroll_item_company_contribution.rb +134 -0
  81. data/lib/qube_sync/operations/payroll_item_deduction.rb +124 -0
  82. data/lib/qube_sync/operations/payroll_item_direct_deposit.rb +70 -0
  83. data/lib/qube_sync/operations/payroll_item_non_wage.rb +93 -0
  84. data/lib/qube_sync/operations/payroll_item_tax.rb +123 -0
  85. data/lib/qube_sync/operations/payroll_item_wage.rb +168 -0
  86. data/lib/qube_sync/operations/payroll_item_workers_compensation.rb +108 -0
  87. data/lib/qube_sync/operations/payroll_last_period.rb +30 -0
  88. data/lib/qube_sync/operations/payroll_liability_adjustment.rb +204 -0
  89. data/lib/qube_sync/operations/payroll_liability_check.rb +137 -0
  90. data/lib/qube_sync/operations/payroll_prior_payment.rb +178 -0
  91. data/lib/qube_sync/operations/payroll_summary_report.rb +157 -0
  92. data/lib/qube_sync/operations/payroll_year_to_date_adjustment.rb +211 -0
  93. data/lib/qube_sync/operations/preferences.rb +61 -0
  94. data/lib/qube_sync/operations/price_level.rb +305 -0
  95. data/lib/qube_sync/operations/purchase_order.rb +1038 -0
  96. data/lib/qube_sync/operations/qb_command_exec.rb +37 -0
  97. data/lib/qube_sync/operations/receive_payment.rb +627 -0
  98. data/lib/qube_sync/operations/receive_payment_to_deposit.rb +38 -0
  99. data/lib/qube_sync/operations/sales_order.rb +1045 -0
  100. data/lib/qube_sync/operations/sales_receipt.rb +1337 -0
  101. data/lib/qube_sync/operations/sales_rep.rb +172 -0
  102. data/lib/qube_sync/operations/sales_tax_code.rb +205 -0
  103. data/lib/qube_sync/operations/sales_tax_payable.rb +37 -0
  104. data/lib/qube_sync/operations/sales_tax_payment_check.rb +375 -0
  105. data/lib/qube_sync/operations/sales_tax_return.rb +35 -0
  106. data/lib/qube_sync/operations/sales_tax_return_line.rb +43 -0
  107. data/lib/qube_sync/operations/ship_method.rb +116 -0
  108. data/lib/qube_sync/operations/special_account.rb +55 -0
  109. data/lib/qube_sync/operations/special_item.rb +44 -0
  110. data/lib/qube_sync/operations/standard_terms.rb +125 -0
  111. data/lib/qube_sync/operations/template.rb +36 -0
  112. data/lib/qube_sync/operations/terms.rb +91 -0
  113. data/lib/qube_sync/operations/time_report.rb +127 -0
  114. data/lib/qube_sync/operations/time_tracking.rb +303 -0
  115. data/lib/qube_sync/operations/to_do.rb +260 -0
  116. data/lib/qube_sync/operations/transaction.rb +356 -0
  117. data/lib/qube_sync/operations/transfer.rb +213 -0
  118. data/lib/qube_sync/operations/transfer_inventory.rb +384 -0
  119. data/lib/qube_sync/operations/txn.rb +38 -0
  120. data/lib/qube_sync/operations/txn_deleted.rb +54 -0
  121. data/lib/qube_sync/operations/txn_display.rb +76 -0
  122. data/lib/qube_sync/operations/txn_void.rb +36 -0
  123. data/lib/qube_sync/operations/unit_of_measure_set.rb +149 -0
  124. data/lib/qube_sync/operations/vehicle.rb +152 -0
  125. data/lib/qube_sync/operations/vehicle_mileage.rb +191 -0
  126. data/lib/qube_sync/operations/vendor.rb +661 -0
  127. data/lib/qube_sync/operations/vendor_credit.rb +1008 -0
  128. data/lib/qube_sync/operations/vendor_type.rb +131 -0
  129. data/lib/qube_sync/operations/workers_comp_code.rb +184 -0
  130. data/lib/qube_sync/operations.rb +1165 -0
  131. data/lib/qube_sync/version.rb +1 -1
  132. data/lib/qube_sync.rb +1 -0
  133. data/sig/qube_sync/client.rbs +82 -0
  134. data/sig/qube_sync/operations/account.rbs +65 -0
  135. data/sig/qube_sync/operations/account_tax_line_info.rbs +13 -0
  136. data/sig/qube_sync/operations/aging_report.rbs +29 -0
  137. data/sig/qube_sync/operations/alert.rbs +53 -0
  138. data/sig/qube_sync/operations/ar_refund_credit_card.rbs +53 -0
  139. data/sig/qube_sync/operations/bar_code.rbs +16 -0
  140. data/sig/qube_sync/operations/bill.rbs +78 -0
  141. data/sig/qube_sync/operations/bill_payment_check.rbs +62 -0
  142. data/sig/qube_sync/operations/bill_payment_credit_card.rbs +45 -0
  143. data/sig/qube_sync/operations/bill_to_pay.rbs +19 -0
  144. data/sig/qube_sync/operations/billing_rate.rbs +40 -0
  145. data/sig/qube_sync/operations/budget_summary_report.rbs +22 -0
  146. data/sig/qube_sync/operations/build_assembly.rbs +64 -0
  147. data/sig/qube_sync/operations/charge.rbs +80 -0
  148. data/sig/qube_sync/operations/check.rbs +81 -0
  149. data/sig/qube_sync/operations/class.rbs +43 -0
  150. data/sig/qube_sync/operations/cleared_status.rbs +16 -0
  151. data/sig/qube_sync/operations/company.rbs +15 -0
  152. data/sig/qube_sync/operations/company_activity.rbs +14 -0
  153. data/sig/qube_sync/operations/credit_card_charge.rbs +69 -0
  154. data/sig/qube_sync/operations/credit_card_credit.rbs +69 -0
  155. data/sig/qube_sync/operations/credit_memo.rbs +162 -0
  156. data/sig/qube_sync/operations/currency.rbs +52 -0
  157. data/sig/qube_sync/operations/custom_detail_report.rbs +31 -0
  158. data/sig/qube_sync/operations/custom_summary_report.rbs +33 -0
  159. data/sig/qube_sync/operations/customer.rbs +174 -0
  160. data/sig/qube_sync/operations/customer_msg.rbs +31 -0
  161. data/sig/qube_sync/operations/customer_type.rbs +32 -0
  162. data/sig/qube_sync/operations/data_event_recovery_info.rbs +20 -0
  163. data/sig/qube_sync/operations/data_ext.rbs +53 -0
  164. data/sig/qube_sync/operations/data_ext_def.rbs +51 -0
  165. data/sig/qube_sync/operations/date_driven_terms.rbs +35 -0
  166. data/sig/qube_sync/operations/deposit.rbs +100 -0
  167. data/sig/qube_sync/operations/employee.rbs +173 -0
  168. data/sig/qube_sync/operations/employee_default.rbs +68 -0
  169. data/sig/qube_sync/operations/entity.rbs +24 -0
  170. data/sig/qube_sync/operations/estimate.rbs +148 -0
  171. data/sig/qube_sync/operations/form1099_category_account_mapping.rbs +33 -0
  172. data/sig/qube_sync/operations/general_detail_report.rbs +31 -0
  173. data/sig/qube_sync/operations/general_summary_report.rbs +32 -0
  174. data/sig/qube_sync/operations/host.rbs +19 -0
  175. data/sig/qube_sync/operations/inventory_adjustment.rbs +105 -0
  176. data/sig/qube_sync/operations/inventory_site.rbs +66 -0
  177. data/sig/qube_sync/operations/invoice.rbs +144 -0
  178. data/sig/qube_sync/operations/item.rbs +25 -0
  179. data/sig/qube_sync/operations/item_assemblies_can_build.rbs +15 -0
  180. data/sig/qube_sync/operations/item_discount.rbs +62 -0
  181. data/sig/qube_sync/operations/item_fixed_asset.rbs +97 -0
  182. data/sig/qube_sync/operations/item_group.rbs +62 -0
  183. data/sig/qube_sync/operations/item_inventory.rbs +87 -0
  184. data/sig/qube_sync/operations/item_inventory_assembly.rbs +94 -0
  185. data/sig/qube_sync/operations/item_non_inventory.rbs +64 -0
  186. data/sig/qube_sync/operations/item_other_charge.rbs +59 -0
  187. data/sig/qube_sync/operations/item_payment.rbs +55 -0
  188. data/sig/qube_sync/operations/item_receipt.rbs +71 -0
  189. data/sig/qube_sync/operations/item_sales_tax.rbs +57 -0
  190. data/sig/qube_sync/operations/item_sales_tax_group.rbs +50 -0
  191. data/sig/qube_sync/operations/item_service.rbs +62 -0
  192. data/sig/qube_sync/operations/item_sites.rbs +26 -0
  193. data/sig/qube_sync/operations/item_subtotal.rbs +48 -0
  194. data/sig/qube_sync/operations/job_report.rbs +28 -0
  195. data/sig/qube_sync/operations/job_type.rbs +32 -0
  196. data/sig/qube_sync/operations/journal_entry.rbs +88 -0
  197. data/sig/qube_sync/operations/lead.rbs +91 -0
  198. data/sig/qube_sync/operations/list.rbs +15 -0
  199. data/sig/qube_sync/operations/list_deleted.rbs +17 -0
  200. data/sig/qube_sync/operations/list_display.rbs +21 -0
  201. data/sig/qube_sync/operations/list_merge.rbs +22 -0
  202. data/sig/qube_sync/operations/other_name.rbs +71 -0
  203. data/sig/qube_sync/operations/paycheck.rbs +27 -0
  204. data/sig/qube_sync/operations/payment_method.rbs +33 -0
  205. data/sig/qube_sync/operations/payroll_detail_report.rbs +29 -0
  206. data/sig/qube_sync/operations/payroll_item.rbs +23 -0
  207. data/sig/qube_sync/operations/payroll_item_addition.rbs +48 -0
  208. data/sig/qube_sync/operations/payroll_item_company_contribution.rbs +58 -0
  209. data/sig/qube_sync/operations/payroll_item_deduction.rbs +54 -0
  210. data/sig/qube_sync/operations/payroll_item_direct_deposit.rbs +30 -0
  211. data/sig/qube_sync/operations/payroll_item_non_wage.rbs +23 -0
  212. data/sig/qube_sync/operations/payroll_item_tax.rbs +58 -0
  213. data/sig/qube_sync/operations/payroll_item_wage.rbs +48 -0
  214. data/sig/qube_sync/operations/payroll_item_workers_compensation.rbs +42 -0
  215. data/sig/qube_sync/operations/payroll_last_period.rbs +13 -0
  216. data/sig/qube_sync/operations/payroll_liability_adjustment.rbs +59 -0
  217. data/sig/qube_sync/operations/payroll_liability_check.rbs +27 -0
  218. data/sig/qube_sync/operations/payroll_prior_payment.rbs +48 -0
  219. data/sig/qube_sync/operations/payroll_summary_report.rbs +30 -0
  220. data/sig/qube_sync/operations/payroll_year_to_date_adjustment.rbs +66 -0
  221. data/sig/qube_sync/operations/preferences.rbs +26 -0
  222. data/sig/qube_sync/operations/price_level.rbs +57 -0
  223. data/sig/qube_sync/operations/purchase_order.rbs +159 -0
  224. data/sig/qube_sync/operations/qb_command_exec.rbs +22 -0
  225. data/sig/qube_sync/operations/receive_payment.rbs +102 -0
  226. data/sig/qube_sync/operations/receive_payment_to_deposit.rbs +15 -0
  227. data/sig/qube_sync/operations/sales_order.rbs +153 -0
  228. data/sig/qube_sync/operations/sales_receipt.rbs +139 -0
  229. data/sig/qube_sync/operations/sales_rep.rbs +43 -0
  230. data/sig/qube_sync/operations/sales_tax_code.rbs +49 -0
  231. data/sig/qube_sync/operations/sales_tax_payable.rbs +16 -0
  232. data/sig/qube_sync/operations/sales_tax_payment_check.rbs +64 -0
  233. data/sig/qube_sync/operations/sales_tax_return.rbs +14 -0
  234. data/sig/qube_sync/operations/sales_tax_return_line.rbs +15 -0
  235. data/sig/qube_sync/operations/ship_method.rbs +31 -0
  236. data/sig/qube_sync/operations/special_account.rbs +16 -0
  237. data/sig/qube_sync/operations/special_item.rbs +17 -0
  238. data/sig/qube_sync/operations/standard_terms.rbs +34 -0
  239. data/sig/qube_sync/operations/template.rbs +15 -0
  240. data/sig/qube_sync/operations/terms.rbs +23 -0
  241. data/sig/qube_sync/operations/time_report.rbs +26 -0
  242. data/sig/qube_sync/operations/time_tracking.rbs +57 -0
  243. data/sig/qube_sync/operations/to_do.rbs +59 -0
  244. data/sig/qube_sync/operations/transaction.rbs +39 -0
  245. data/sig/qube_sync/operations/transfer.rbs +47 -0
  246. data/sig/qube_sync/operations/transfer_inventory.rbs +76 -0
  247. data/sig/qube_sync/operations/txn.rbs +15 -0
  248. data/sig/qube_sync/operations/txn_deleted.rbs +17 -0
  249. data/sig/qube_sync/operations/txn_display.rbs +22 -0
  250. data/sig/qube_sync/operations/txn_void.rbs +15 -0
  251. data/sig/qube_sync/operations/types.rbs +434 -0
  252. data/sig/qube_sync/operations/unit_of_measure_set.rbs +51 -0
  253. data/sig/qube_sync/operations/vehicle.rbs +43 -0
  254. data/sig/qube_sync/operations/vehicle_mileage.rbs +38 -0
  255. data/sig/qube_sync/operations/vendor.rbs +142 -0
  256. data/sig/qube_sync/operations/vendor_credit.rbs +70 -0
  257. data/sig/qube_sync/operations/vendor_type.rbs +32 -0
  258. data/sig/qube_sync/operations/workers_comp_code.rbs +52 -0
  259. data/sig/qube_sync/operations.rbs +147 -0
  260. metadata +255 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d91d913272b0ecdd7f2b2bf91b584e2ab2291841a3badf4641d67d385841b51
4
- data.tar.gz: 00d4062eead7a747ddc51ee0eaf44f5e25799532028b3ed30343ec25f7e7843e
3
+ metadata.gz: 1eac592828778d486630d76ff7d8b43072a5006682c1b8182f1c5e13f48097f2
4
+ data.tar.gz: 04de6f5cc7db7be2569186733e1961f291348d96e8a23dcf832ab4f40a96d722
5
5
  SHA512:
6
- metadata.gz: b244afce7cc38609ca0b100fb4f85102c13ca194d7d5cfd61d792950c37402cf84dd28e868830be11060bf5948482607ef206749c9b6c5b7f302da9df2883db6
7
- data.tar.gz: 923cf67564f27f9be8a715ec059bdb4dfa70befc478eaeba7188bfe3a02c203b21b63d68e4db827c3d55727c4070c4c3d7f2c229f603dc75e109a7e6d9e7db8f
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
- A simple wrapper around the QUBE Sync API.
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
- ## Configuration
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
- ## Usage
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