qube_rails 0.2.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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +161 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +967 -0
  5. data/app/assets/javascripts/qube_rails/question.js +457 -0
  6. data/app/assets/stylesheets/qube_rails.css +1115 -0
  7. data/app/controllers/concerns/qube_rails/run_authorization.rb +37 -0
  8. data/app/controllers/qube_rails/connections_controller.rb +141 -0
  9. data/app/controllers/qube_rails/questions_controller.rb +132 -0
  10. data/app/controllers/qube_rails/runs_controller.rb +54 -0
  11. data/app/controllers/qube_rails/storyboard_controller.rb +50 -0
  12. data/app/controllers/qube_rails/webhooks_controller.rb +99 -0
  13. data/app/jobs/qube_rails/prune_runs_job.rb +18 -0
  14. data/app/jobs/qube_rails/refresh_job.rb +18 -0
  15. data/app/jobs/qube_rails/webhook_job.rb +43 -0
  16. data/app/models/qube_rails/application_record.rb +17 -0
  17. data/app/models/qube_rails/connection.rb +303 -0
  18. data/app/models/qube_rails/notification.rb +12 -0
  19. data/app/models/qube_rails/queued_request.rb +66 -0
  20. data/app/models/qube_rails/quickbooks_record.rb +36 -0
  21. data/app/models/qube_rails/request_delivery.rb +12 -0
  22. data/app/models/qube_rails/run.rb +187 -0
  23. data/app/models/qube_rails/sync_mark.rb +21 -0
  24. data/app/views/qube_rails/_error.html.erb +13 -0
  25. data/app/views/qube_rails/_question.html.erb +21 -0
  26. data/app/views/qube_rails/_run_status.html.erb +22 -0
  27. data/app/views/qube_rails/question/_choice.html.erb +20 -0
  28. data/app/views/qube_rails/question/_details.html.erb +6 -0
  29. data/app/views/qube_rails/question/_field.html.erb +15 -0
  30. data/app/views/qube_rails/question/_footer.html.erb +7 -0
  31. data/app/views/qube_rails/question/_message.html.erb +2 -0
  32. data/app/views/qube_rails/question/_pick.html.erb +19 -0
  33. data/app/views/qube_rails/question/_picks.html.erb +6 -0
  34. data/app/views/qube_rails/question/_question.html.erb +24 -0
  35. data/app/views/qube_rails/question/fields/_boolean.html.erb +7 -0
  36. data/app/views/qube_rails/question/fields/_number.html.erb +7 -0
  37. data/app/views/qube_rails/question/fields/_rows.html.erb +23 -0
  38. data/app/views/qube_rails/question/fields/_search.html.erb +23 -0
  39. data/app/views/qube_rails/question/fields/_select.html.erb +8 -0
  40. data/app/views/qube_rails/question/fields/_text.html.erb +7 -0
  41. data/app/views/qube_rails/question/rows/_cell.html.erb +24 -0
  42. data/app/views/qube_rails/question/rows/_row.html.erb +22 -0
  43. data/app/views/qube_rails/question/rows/_segmented.html.erb +14 -0
  44. data/app/views/qube_rails/run_status/_detail.html.erb +7 -0
  45. data/app/views/qube_rails/run_status/_redeliver.html.erb +5 -0
  46. data/app/views/qube_rails/run_status/_retry.html.erb +4 -0
  47. data/app/views/qube_rails/run_status/_stall.html.erb +3 -0
  48. data/app/views/qube_rails/run_status/_status.html.erb +21 -0
  49. data/app/views/qube_rails/run_status/_summary.html.erb +2 -0
  50. data/app/views/qube_rails/storyboard/show.html.erb +86 -0
  51. data/config/locales/en.yml +46 -0
  52. data/config/routes.rb +35 -0
  53. data/db/migrate/20261025000000_create_qube_tables.rb +83 -0
  54. data/db/migrate/20261026230000_add_qube_sync_status_indexes.rb +13 -0
  55. data/db/migrate/20261027000000_create_qube_sync_marks.rb +17 -0
  56. data/db/migrate/20261028000000_create_qube_requests.rb +38 -0
  57. data/lib/generators/qube_rails/install_generator.rb +47 -0
  58. data/lib/generators/qube_rails/templates/connection_events.rb.tt +38 -0
  59. data/lib/generators/qube_rails/templates/initializer.rb.tt +82 -0
  60. data/lib/generators/qube_rails/workflows_generator.rb +77 -0
  61. data/lib/qube_rails/backend.rb +70 -0
  62. data/lib/qube_rails/cache_dedupe.rb +34 -0
  63. data/lib/qube_rails/configuration.rb +319 -0
  64. data/lib/qube_rails/connection_events.rb +167 -0
  65. data/lib/qube_rails/database_dedupe.rb +28 -0
  66. data/lib/qube_rails/engine.rb +33 -0
  67. data/lib/qube_rails/json_coder.rb +43 -0
  68. data/lib/qube_rails/operation.rb +464 -0
  69. data/lib/qube_rails/question_view.rb +219 -0
  70. data/lib/qube_rails/request.rb +289 -0
  71. data/lib/qube_rails/rows_view.rb +146 -0
  72. data/lib/qube_rails/run_status_view.rb +71 -0
  73. data/lib/qube_rails/storyboard/ask_about_add.json +60 -0
  74. data/lib/qube_rails/storyboard/choose.json +51 -0
  75. data/lib/qube_rails/storyboard/enum_select.json +56 -0
  76. data/lib/qube_rails/storyboard/missing_record.json +141 -0
  77. data/lib/qube_rails/storyboard/name_clash.json +59 -0
  78. data/lib/qube_rails/storyboard/near_match.json +77 -0
  79. data/lib/qube_rails/storyboard/number_boolean.json +54 -0
  80. data/lib/qube_rails/storyboard/record_search.json +90 -0
  81. data/lib/qube_rails/storyboard/refusal.json +53 -0
  82. data/lib/qube_rails/storyboard/rows.json +359 -0
  83. data/lib/qube_rails/storyboard.rb +165 -0
  84. data/lib/qube_rails/sync_status.rb +124 -0
  85. data/lib/qube_rails/version.rb +5 -0
  86. data/lib/qube_rails/view_helpers.rb +29 -0
  87. data/lib/qube_rails.rb +201 -0
  88. metadata +180 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: deb2eb2f7213591f12c48f2ecbf23e1eece273630d9b716b233c338fa4572a37
4
+ data.tar.gz: eeacddeebc0117e15dd7096d881d9a818eade8485a2d12f0e6565e0a5e015e56
5
+ SHA512:
6
+ metadata.gz: 1989097d43b0a33d92b8adf5059d50d360250e5f967ca5bf60aa1e62b7703131fe95d2a21a803a78a1861d71ccb26b7cd5eda8cc8abaff384c70249091d00047
7
+ data.tar.gz: b8bc4a5015c9e93dd4433222aedbd842469691ed69c2bcac393d6743156ed792ec7b41fa9cdae571f1b635f48ccb84792264de18846bf5ea2b3b58f4a612eae9
data/CHANGELOG.md ADDED
@@ -0,0 +1,161 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 (2026-10-27)
4
+
5
+ A rewrite on QuBe Sync's v2 API. The engine now holds the code QuBe Sync's integration kit used to
6
+ generate into every Rails app (the webhook endpoint, onboarding, questions, connections, runs and
7
+ QuickBooks ids), so your app keeps only its own operations, its connection events class and one
8
+ initializer, and gets fixes with `bundle update qube_rails`. Nothing from 0.1.0 carries over.
9
+
10
+ ### Added
11
+
12
+ **Install and configure**
13
+ - `rails g qube_rails:install`: the initializer, your app's `QuickbooksDesktop::ConnectionEvents`,
14
+ the engine's migrations and the mount line (`mount QubeRails::Engine => "/"`).
15
+ - `QubeRails.configure` in `config/initializers/qube_rails.rb`: operations, connection_events,
16
+ current_account, authorize_question, enqueue, queue, dedupe, parent_controller and the API
17
+ settings. A setting left unset raises `QubeRails::NotConfigured` naming that file.
18
+
19
+ **Connections and onboarding**
20
+ - Engine tables and models, from `bin/rails qube_rails:install:migrations`:
21
+ `QubeRails::Connection` (`qube_connections`; an account can have any number, one per company
22
+ file), `QubeRails::Run` (`qube_runs`: each run's reference, revision, state and question) and
23
+ `QubeRails::QuickbooksRecord` (`qube_quickbooks_records`: QuickBooks ids by connection). Your
24
+ own tables need no QuBe Sync columns. JSON is stored as text, the same on PostgreSQL, MySQL and
25
+ SQLite.
26
+ - Onboarding routes: `POST quickbooks/connections` (Connect) and
27
+ `POST quickbooks/connections/:id/onboarding`. Connect reuses the account's newest
28
+ never-connected connection; `new=1` makes another.
29
+ - Onboarding makes realtime connections (`config.connection_type`, `"realtime"` by default),
30
+ since workflows run only on realtime connections.
31
+ - Connect with `simulated=1`, outside production, makes a connection to a simulated QuickBooks
32
+ company file, connected as soon as it's made, for testing without a Windows computer.
33
+ - A connection event for a row that doesn't have its QuBe Sync id yet adopts the QuBe Sync
34
+ connection made with that row's webhook URL and name, created at or after the row (a
35
+ never-connected or simulated one), or raises `QubeRails::NotReady` to be retried.
36
+ - `DELETE quickbooks/connections/:id` (`connection_path`) disconnects a connection: QuBe Sync's
37
+ first, then the row with its runs, requests and links (`Connection#disconnect!`), after
38
+ `ConnectionEvents#disconnected`, where your app deletes its own rows that point at it.
39
+
40
+ **Starting workflows**
41
+ - `QubeRails::Operation`, the base of your operations: `start(record, connection)` returns the
42
+ run. A record's run that hasn't ended on that connection is returned (`run.reused?`) rather
43
+ than a second one started, so a double click can't add an invoice twice; an unconfirmed start
44
+ is sent again with the same reference (the Idempotency-Key) and input.
45
+ - A start that gets no answer stays `starting` with its input; `Run#retry_start!` and
46
+ `Operation.retry_start` resend it with the same Idempotency-Key. Only a 4xx, or an input that
47
+ raised, marks a run failed.
48
+ - `Operation#quickbooks_ref` and `#link_references`, and `QubeRails::MissingReference`, raised
49
+ from `input` when a reference QuickBooks requires has no link and no name, so the start fails
50
+ before anything is sent.
51
+ - Typed workflow starts: `bin/rails g qube_rails:workflows [--drafts] [--check]` writes
52
+ `app/qube/qube_workflows.rb` (`QubeWorkflows`, autoloaded) and `sig/qube_workflows.rbs` from
53
+ your app's workflow input types, with a typed start and input method per workflow and branch.
54
+ `Operation#input` can return one of those input methods' Hashes. `--check` exits 1 when the
55
+ file is out of date or the API key is refused, and warns but passes when QuBe Sync can't be
56
+ reached.
57
+ - A start QuBe Sync refuses because the connection isn't realtime raises
58
+ `QubeRails::RealtimeRequired`, whose message says how to upgrade.
59
+
60
+ **Plain requests**
61
+ - `QubeRails::Request`, the base of a plain v2 request (`OPERATION`, `SLUG`, `params`, and the
62
+ handlers `answered`, `done`, `failed`, `retrying`, `timed_out`). `.queue(record, connection)`
63
+ returns a `QubeRails::QueuedRequest`: one at a time per record, slug and connection, its
64
+ reference the Idempotency-Key, `retry_queue!` to send it again. Their webhooks arrive at
65
+ `POST webhooks/qube/requests/:id`; each delivery is taken once by `page_request_id:state`, and
66
+ `done` fires once every page is in.
67
+
68
+ **Webhooks**
69
+ - The webhook endpoint, `POST webhooks/qube/runs/:id` and `POST webhooks/qube/connections/:id`,
70
+ verifies each delivery and hands it to `QubeRails::WebhookJob` (ActiveJob), or to
71
+ `QubeRails.run(kind, id, body)` from your own queue. It answers 503 when the enqueue fails, so
72
+ QuBe Sync retries. A refused delivery's log line names its path and what to check (the webhook
73
+ secret, the clock, or a sender that isn't QuBe Sync).
74
+ - The reference, connection id, revision and delivery id are checked under the row's lock, and
75
+ notifications are deduplicated in the engine's `qube_notifications` table in the same
76
+ transaction as your handler, so a handler that raises runs again on the redelivery
77
+ (`QubeRails::CacheDedupe` is an option). `WebhookJob` retries (`job_attempts`, default 10) and
78
+ logs when it gives up. A delivery for a deleted run, connection or record is logged and dropped.
79
+ - `QubeRails::ConnectionEvents`, the base of your connection events class.
80
+
81
+ **Questions**
82
+ - The question routes, `POST qube/questions/runs/:id` and `GET .../search`, and the
83
+ `qube_rails/_question.html.erb` partial showing a record's latest run's question, which you can
84
+ override. `authorize_question` defaults to allowing only the account that owns the run's
85
+ connection; a refused one answers 404.
86
+ - When a question changed after the page showed it, the answer isn't sent and the page says
87
+ where it stands now.
88
+ - A rows field (the missing-records question) shows a block per record: its choice ("Create it",
89
+ "Use an existing one") as a segmented control, then only the fields that choice needs, with a
90
+ search per record column. "Set all to" and "Use for all N" fill every record at once. It never
91
+ scrolls sideways. A fixed list shows as a select.
92
+ - A question whose answer QuBe Sync took is `running` again at once (`Operation#answered(token,
93
+ taken: true)`), rather than waiting for the Web Connector's next poll.
94
+
95
+ **Runs and sync status**
96
+ - Every ending is kept on the run (state, outcome, error, cancel reason, ended at) before your
97
+ handler runs: `Run#ended?`, `#failed?`, `#cancelled?`, `#summary_text`, and the
98
+ `qube_rails/run_status` partial (taking `connection:`). A start unconfirmed for a minute reads
99
+ "Still waiting for QuickBooks" with a "Try sending again" button
100
+ (`POST qube/runs/:id/retry_start`).
101
+ - A run that looks stuck (`Run#looks_stuck?`) gets a "Check for updates" button, which has QuBe
102
+ Sync send its latest event again (`POST qube/runs/:id/redeliver`).
103
+ - While the connection a sending run waits on is stalled, the status reads "Waiting for
104
+ QuickBooks" and says why and what to do. `connection.stalled` and `connection.resumed` refresh
105
+ the pages of records with a run going on that connection.
106
+ - Sync status, derived from runs and links and never stored:
107
+ `connection.sync_status(record, operation:)` (`QubeRails::SyncStatus`: `:not_sent`,
108
+ `:sending`, `:needs_answer`, `:in_quickbooks`, `:done`, `:failed`, `:cancelled`, with `qb_id`,
109
+ `error` and `retry?`), `SyncStatus.latest`, and the connection's scopes `in_quickbooks`,
110
+ `not_in_quickbooks`, `needing_answer` and `sending`. Tested on SQLite, PostgreSQL and MySQL.
111
+ - The partials show why an answer or a retried start didn't go through, and with turbo-rails 2
112
+ a record's pages refresh when an event changes its run, and once more 5 seconds later
113
+ (`QubeRails::RefreshJob`) for a page that was still loading.
114
+ - `config.prune_runs_after` and `QubeRails::PruneRunsJob`: opt-in pruning of ended runs and
115
+ requests (`Run.prunable`), never within a day of their end; links stay. `qube_sync_marks` keeps
116
+ when each connection's last completed run of a workflow started
117
+ (`Connection#last_synced_at(workflow)`, `Operation#last_synced_at`), so a sync's next `since`
118
+ outlives pruned runs.
119
+
120
+ **Screens and styling** (README, "Styling")
121
+ - The question and run status partials (`render "qube_rails/question"`, `render
122
+ "qube_rails/run_status"`) are made of small pieces under `qube_rails/question/*`,
123
+ `qube_rails/run_status/*` and `qube_rails/_error`, each overridable at its path and rendered
124
+ from view-models whose fields are the contract (`QubeRails::QuestionView`, `ChoiceView`,
125
+ `FieldView`, `RowsView`, `RowView`, `CellView`, `RunStatusView`). A question's choices show by
126
+ kind: suggested records as option rows, each form in its own section with the one filled
127
+ button, and "None of these" and "Cancel" in a footer.
128
+ - `config.ui_classes`: your CSS framework's classes by piece, beside the `qube-` classes; an
129
+ unknown piece raises.
130
+ - `<%= qube_rails_assets %>` in your layout (`rails g qube_rails:install` adds it) serves the
131
+ behaviour script `qube_rails/question.js` (search boxes, rows fields), keyed only on
132
+ `data-qube-*` attributes, so it works on any markup, after Turbo navigation, with no CSP nonce.
133
+ `qube_rails_assets stylesheet: true` adds the opt-in stylesheet `qube_rails.css`: zero
134
+ specificity, themed by `--qube-*` variables, light and dark, no global styles.
135
+ - The screens never name QuBe Sync, runs, webhooks or ListIDs to your users, and every word comes
136
+ from I18n under `qube_rails.*` (`config/locales/en.yml`; `QubeRails.t`), so you can reword or
137
+ translate them.
138
+ - The storyboard, `GET qube/storyboard(/:screen)` (`config.storyboard`, on in development):
139
+ every screen from sample data, in your layout, with each piece's override path. Nothing is sent
140
+ or saved.
141
+
142
+ **QuickBooks ids**
143
+ - `Connection#link`, `#qb_id_for`, `#find_linked(qb_id, kind: nil)` and
144
+ `#quickbooks_record(qb_id, kind: nil)`. QuickBooks ids are kept by connection and list family
145
+ (`QuickbooksRecord.family_for`: one family for customers, vendors, employees and other names,
146
+ one for items, and each other list or transaction type its own), since QuickBooks gives out
147
+ ListIDs that way.
148
+
149
+ ### Changed
150
+ - Needs Ruby 3.3, Rails 7.0 and qube_sync 0.5.0 or later. The source stays Ruby 2.7 and Rails
151
+ 5.2 compatible, and every call to qube_sync goes through `QubeRails::Backend`, so the kit can
152
+ vendor it into older apps.
153
+
154
+ ### Removed
155
+ - Everything from 0.1.0 (v1): the generator's models, migrations, controllers and jobs, and the
156
+ path dependency on qube_sync.
157
+
158
+ ## 0.1.0 (2024-01-21)
159
+
160
+ The v1 engine: generated QuickBooks entity models, connection and request tables, and a webhook
161
+ job for customer queries.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 QUBE Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.