printavo-ruby 0.17.0 → 0.18.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59696533b085e116847901e6b03e9d3687e1a7485b513e350452351d990d9ed2
4
- data.tar.gz: b435b6411596af3cb65310f2172f83d25d9ee1e0f317d05d0ad1a25f975c27ad
3
+ metadata.gz: 5b7e5a10f0b5ecc844c1625db1ebae2f19154c81d49a666af5d5f4e6f9d55bd8
4
+ data.tar.gz: 8a75223829bbb6a67c9477460556b78260ef99104ab7e5ef9af7bdcc64c9b558
5
5
  SHA512:
6
- metadata.gz: 0d08433f369b1f5d5943d089558daf52c875ca74d0d7e377ea7b93857b2bc7d905699a21d627a0d23c9ad398d8131a078ec509c36d7d83efb8175c98e877f956
7
- data.tar.gz: 9c28bdd515691f4f5ec0b211883a42372cfa28df76f23a2b9608ea9a871b83340f8cca539360747a1e16b9274aa1c9593ab434db3f06f700535c89361504e009
6
+ metadata.gz: 4a92c64b1c47d1f9aab27a2fb29957337f582fe4b59cfb675ee937fcd8ef18c4ebf078479fe8cbceb608589648636be5e74daf40f95e0c1d1df63c8d359bb106
7
+ data.tar.gz: 90c15ea027dfffa37c57aab86086edee3624feb5b31cd4bec431cf41d250b3296bd097f388e69609f13077db7c46fc8db047aaaddbce7a7964d16f7b4b108b74
data/docs/CACHING.md CHANGED
@@ -205,26 +205,81 @@ how frequently staff update records.
205
205
 
206
206
  ---
207
207
 
208
- ## Future: Built-In Cache Adapter
208
+ ## Built-In Cache Adapter
209
209
 
210
- A future version of `printavo-ruby` may support an optional cache adapter
211
- passed directly to the client:
210
+ As of v0.18.0, `Printavo::Client` accepts an optional `cache:` argument — any
211
+ object responding to `fetch(key, expires_in:) { }` and `delete(key)`. This
212
+ matches the `Rails.cache` interface, so no adapter is needed in Rails apps.
213
+
214
+ ### With Rails.cache
215
+
216
+ ```ruby
217
+ client = Printavo::Client.new(
218
+ email: ENV["PRINTAVO_EMAIL"],
219
+ token: ENV["PRINTAVO_TOKEN"],
220
+ cache: Rails.cache, # Memcache, Redis, Solid Cache — anything Rails supports
221
+ default_ttl: 300 # seconds; default is 300 (5 minutes)
222
+ )
223
+
224
+ # All queries are automatically cached and deduped:
225
+ client.customers.all # fetches from API, stores result
226
+ client.customers.all # returns cached result — no HTTP request
227
+ ```
228
+
229
+ ### With Printavo::MemoryStore (no Rails dependency)
212
230
 
213
231
  ```ruby
214
- # Possible future API — not implemented in v0.x
215
232
  client = Printavo::Client.new(
216
233
  email: ENV["PRINTAVO_EMAIL"],
217
234
  token: ENV["PRINTAVO_TOKEN"],
218
- cache: Rails.cache, # any object responding to fetch/delete
219
- default_ttl: 300
235
+ cache: Printavo::MemoryStore.new,
236
+ default_ttl: 600
220
237
  )
238
+ ```
239
+
240
+ `Printavo::MemoryStore` is a thread-safe, TTL-aware in-memory store included
241
+ in the gem. No configuration required.
221
242
 
222
- # Would cache automatically:
223
- client.customers.all # cached for 300s by default
224
- client.orders.find(1) # cached per-id
243
+ ### Without a cache (default)
244
+
245
+ ```ruby
246
+ client = Printavo::Client.new(
247
+ email: ENV["PRINTAVO_EMAIL"],
248
+ token: ENV["PRINTAVO_TOKEN"]
249
+ )
250
+ # cache: nil — every query hits the API
251
+ ```
252
+
253
+ ### Cache key generation
254
+
255
+ Cache keys are generated from a SHA-256 digest of the normalized query document
256
+ and variables: `"printavo:gql:<16-hex-chars>"`. Whitespace differences in query
257
+ strings do not cause cache misses.
258
+
259
+ ### Mutations are never cached
260
+
261
+ `client.graphql.mutate(...)` always bypasses the cache, regardless of the
262
+ `cache:` setting. Only `client.graphql.query(...)` (and resource methods that
263
+ call it) are cache-aware.
264
+
265
+ ### Custom cache stores
266
+
267
+ Any object with this interface works:
268
+
269
+ ```ruby
270
+ class MyCache
271
+ def fetch(key, expires_in: nil)
272
+ # return cached value, or call block and store the result
273
+ end
274
+
275
+ def delete(key)
276
+ # remove key from cache
277
+ end
278
+ end
225
279
  ```
226
280
 
227
- Track this feature in [FUTURE.md](../FUTURE.md).
281
+ This is exactly the interface `ActiveSupport::Cache::Store` exposes, so
282
+ Solid Cache, Dalli (Memcache), and Redis Cache Store all work out of the box.
228
283
 
229
284
  ---
230
285
 
data/docs/FUTURE.md CHANGED
@@ -6,66 +6,35 @@ initial `0.x` releases. Contributions and discussions welcome!
6
6
 
7
7
  ## Planned Features
8
8
 
9
- ### CLI (Thor-based)
9
+ ### Client-Side Aggregation Helpers
10
10
 
11
- A `printavo` command-line tool built with [Thor](https://github.com/rails/thor):
11
+ Printavo's V2 GraphQL API is a transactional API — it has no pre-aggregated
12
+ analytics or reporting endpoints. Any analytics must be computed by paging
13
+ through existing resources in Ruby.
12
14
 
13
- ```bash
14
- printavo customers
15
- printavo orders
16
- printavo orders find 12345
17
- printavo analytics revenue
18
- printavo sync orders --to crm
19
- ```
20
-
21
- Planned version: `0.7.0`
22
-
23
- ### Retry/Backoff
24
-
25
- Intelligent rate limit handling with exponential backoff:
15
+ Potential helpers that would add value:
26
16
 
27
17
  ```ruby
28
- client = Printavo::Client.new(
29
- email: ENV["PRINTAVO_EMAIL"],
30
- token: ENV["PRINTAVO_TOKEN"],
31
- max_retries: 3,
32
- retry_on_rate_limit: true
33
- )
34
- ```
35
-
36
- Planned version: `0.8.0`
37
-
38
- ### Analytics / Reporting Expansion
39
-
40
- Richer wrappers for Printavo's analytics queries (revenue, job counts,
41
- customer activity, turnaround times).
18
+ # Revenue across all invoices in a date range
19
+ client.invoices.revenue_summary(after: "2026-01-01")
20
+ # => { total: "142300.00", count: 87, average: "1636.78" }
42
21
 
43
- Planned version: `0.6.0`
22
+ # Order counts grouped by status
23
+ client.orders.status_breakdown
24
+ # => { in_production: 12, approved: 5, completed: 230, ... }
44
25
 
45
- ### Mutations (Create / Update)
26
+ # Most active customers by order count
27
+ client.customers.top(limit: 10, by: :order_count)
46
28
 
47
- Support for creating and updating resources:
48
-
49
- ```ruby
50
- client.customers.create(first_name: "Jane", last_name: "Smith", email: "jane@example.com")
51
- client.orders.update("99", nickname: "Rush Job")
29
+ # Average turnaround time (created_at → updated_at) per status
30
+ client.orders.avg_turnaround
52
31
  ```
53
32
 
54
- Planned version: `0.5.0`
55
-
56
- ### Built-In Cache Adapter
57
-
58
- Optional cache layer that plugs into any cache store:
59
-
60
- ```ruby
61
- client = Printavo::Client.new(
62
- email: ENV["PRINTAVO_EMAIL"],
63
- token: ENV["PRINTAVO_TOKEN"],
64
- cache: Rails.cache # or a Redis client, etc.
65
- )
66
- ```
33
+ These helpers would page all relevant records locally and compute aggregates
34
+ in Ruby. Because they require full pagination, using the built-in cache adapter
35
+ is strongly recommended before implementing these in production workflows.
67
36
 
68
- See [docs/CACHING.md](docs/CACHING.md) for current caching recommendations.
37
+ See [docs/CACHING.md](docs/CACHING.md) for caching options.
69
38
 
70
39
  ## Visualization
71
40
 
data/docs/TODO.md CHANGED
@@ -402,6 +402,21 @@ return values — they are exposed via model field accessors, not separate resou
402
402
  - [x] `Printavo::Enums::TransactionCategory`
403
403
  - [x] `Printavo::Enums::TransactionSource`
404
404
 
405
+ ### v0.17.0 — GraphQL Interface Field Coverage ✅
406
+
407
+ - [x] `VisualIDed` interface — `visualId` on all applicable models
408
+ - [x] `Timestamps` interface — `createdAt` / `updatedAt` on all applicable models
409
+ - [x] `MailAddress` interface — address fields on all applicable models
410
+
411
+ ### v0.18.0 — Built-In Cache Adapter ✅
412
+
413
+ - [x] Optional `cache:` kwarg on `Printavo::Client` (any `Rails.cache`-compatible store)
414
+ - [x] `Printavo::MemoryStore` — thread-safe TTL-aware in-memory store (no Rails dependency)
415
+ - [x] `GraphqlClient` caching: `#query` is cache-aware; `#mutate` always bypasses
416
+ - [x] Stable SHA-256 cache key from normalized query + variables
417
+ - [x] `default_ttl: 300` configurable per-client
418
+ - [x] `docs/CACHING.md` updated with built-in adapter usage examples
419
+
405
420
  ### v0.99.0 — API Freeze
406
421
 
407
422
  - [ ] Community feedback integration
@@ -418,11 +433,26 @@ return values — they are exposed via model field accessors, not separate resou
418
433
 
419
434
  ## Future / Stretch Goals
420
435
 
421
- ### Built-In Cache Adapter
436
+ ### Client-Side Aggregation Helpers
437
+
438
+ > **Note:** Printavo's V2 GraphQL API exposes no pre-aggregated analytics or
439
+ > reporting endpoints. All "analytics" must be computed locally by paging
440
+ > through existing resources. A cache adapter (see below) is a prerequisite
441
+ > before these helpers would be practical in production.
422
442
 
423
- - [ ] Optional `cache:` kwarg on `Printavo::Client`
424
- - [ ] Adapter interface: `read(key)` / `write(key, value, ttl:)` / `delete(key)`
425
- - [ ] `Rails.cache`, Redis, and in-memory adapter support
443
+ - [ ] `Invoices#revenue_summary(after:, before:)` page invoices, return total/count/average
444
+ - [ ] `Orders#status_breakdown` group order count by status key
445
+ - [ ] `Customers#top(limit:, by:)` rank customers by order count or revenue
446
+ - [ ] `Orders#avg_turnaround` — average time from `created_at` to most recent `updated_at`
447
+
448
+ ### Built-In Cache Adapter ✅ (v0.18.0)
449
+
450
+ - [x] Optional `cache:` kwarg on `Printavo::Client`
451
+ - [x] Duck-typed adapter interface: `fetch(key, expires_in:) { }` / `delete(key)` — matches `Rails.cache`
452
+ - [x] `Printavo::MemoryStore` — thread-safe in-memory store for non-Rails usage
453
+ - [x] `GraphqlClient#query` is cache-aware; `#mutate` always bypasses cache
454
+ - [x] Stable SHA-256 cache key from normalized query + variables
455
+ - [x] `default_ttl: 300` configurable per-client
426
456
 
427
457
  ### Workflow Diagram Generation
428
458
 
@@ -430,13 +460,6 @@ return values — they are exposed via model field accessors, not separate resou
430
460
  - [ ] `ruby-graphviz` backend (DOT → SVG/PNG)
431
461
  - [ ] Mermaid output option (embed in GitHub markdown)
432
462
 
433
- ### Multi-Language SDK Family
434
-
435
- - [ ] `printavo-python`
436
- - [ ] `printavo-swift`
437
- - [ ] `printavo-zig`
438
- - [ ] `printavo-odin`
439
-
440
463
  ---
441
464
 
442
465
 
@@ -8,21 +8,38 @@ module Printavo
8
8
  # Creates a new Printavo API client. Each instance is independent —
9
9
  # multiple clients with different credentials can coexist in one process.
10
10
  #
11
- # @param email [String] the email address associated with your Printavo account
12
- # @param token [String] the API token from your Printavo My Account page
13
- # @param timeout [Integer] HTTP timeout in seconds (default: 30)
14
- # @param max_retries [Integer] max retry attempts on 5xx/429 responses (default: 2)
15
- # @param retry_on_rate_limit [Boolean] retry automatically on 429 Too Many Requests (default: true)
11
+ # @param email [String] the email address associated with your Printavo account
12
+ # @param token [String] the API token from your Printavo My Account page
13
+ # @param timeout [Integer] HTTP timeout in seconds (default: 30)
14
+ # @param max_retries [Integer] max retry attempts on 5xx/429 responses (default: 2)
15
+ # @param retry_on_rate_limit [Boolean] retry automatically on 429 Too Many Requests (default: true)
16
+ # @param cache [#fetch, #delete] optional cache store; any object responding to
17
+ # +fetch(key, expires_in:) { }+ and +delete(key)+,
18
+ # e.g. +Rails.cache+ or +Printavo::MemoryStore.new+
19
+ # @param default_ttl [Integer] TTL in seconds for cached queries (default: 300)
16
20
  #
17
- # @example
21
+ # @example No caching (default)
18
22
  # client = Printavo::Client.new(
19
23
  # email: ENV["PRINTAVO_EMAIL"],
20
24
  # token: ENV["PRINTAVO_TOKEN"]
21
25
  # )
22
- # client.customers.all
23
- # client.orders.find("12345")
24
- # client.graphql.query("{ customers { nodes { id } } }")
25
- def initialize(email:, token:, timeout: 30, max_retries: 2, retry_on_rate_limit: true)
26
+ #
27
+ # @example With Rails.cache
28
+ # client = Printavo::Client.new(
29
+ # email: ENV["PRINTAVO_EMAIL"],
30
+ # token: ENV["PRINTAVO_TOKEN"],
31
+ # cache: Rails.cache,
32
+ # default_ttl: 300
33
+ # )
34
+ #
35
+ # @example With built-in in-memory store
36
+ # client = Printavo::Client.new(
37
+ # email: ENV["PRINTAVO_EMAIL"],
38
+ # token: ENV["PRINTAVO_TOKEN"],
39
+ # cache: Printavo::MemoryStore.new
40
+ # )
41
+ def initialize(email:, token:, timeout: 30, max_retries: 2, retry_on_rate_limit: true, # rubocop:disable Metrics/ParameterLists
42
+ cache: nil, default_ttl: 300)
26
43
  connection = Connection.new(
27
44
  email: email,
28
45
  token: token,
@@ -30,7 +47,7 @@ module Printavo
30
47
  max_retries: max_retries,
31
48
  retry_on_rate_limit: retry_on_rate_limit
32
49
  ).build
33
- @graphql = GraphqlClient.new(connection)
50
+ @graphql = GraphqlClient.new(connection, cache: cache, default_ttl: default_ttl)
34
51
  end
35
52
 
36
53
  def account
@@ -1,12 +1,20 @@
1
1
  # lib/printavo/graphql_client.rb
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'digest'
4
5
  require 'json'
5
6
 
6
7
  module Printavo
7
8
  class GraphqlClient
8
- def initialize(connection)
9
- @connection = connection
9
+ # @param connection [Faraday::Connection]
10
+ # @param cache [#fetch, #delete, nil] any cache store implementing
11
+ # +fetch(key, expires_in:) { }+ and +delete(key)+,
12
+ # e.g. +Rails.cache+, +Printavo::MemoryStore.new+, or +nil+
13
+ # @param default_ttl [Integer] default TTL in seconds applied to cached queries (default: 300)
14
+ def initialize(connection, cache: nil, default_ttl: 300)
15
+ @connection = connection
16
+ @cache = cache
17
+ @default_ttl = default_ttl
10
18
  end
11
19
 
12
20
  # Executes a GraphQL query and returns the parsed `data` hash.
@@ -22,7 +30,11 @@ module Printavo
22
30
  # variables: { id: "42" }
23
31
  # )
24
32
  def query(query_string, variables: {})
25
- execute(query_string, variables: variables)
33
+ return execute(query_string, variables: variables) unless @cache
34
+
35
+ @cache.fetch(cache_key(query_string, variables), expires_in: @default_ttl) do
36
+ execute(query_string, variables: variables)
37
+ end
26
38
  end
27
39
 
28
40
  # Executes a GraphQL mutation and returns the parsed `data` hash.
@@ -86,6 +98,13 @@ module Printavo
86
98
 
87
99
  private
88
100
 
101
+ # Generates a stable, namespaced cache key from the query document and variables.
102
+ # Whitespace in the query is collapsed so formatting differences don't cause misses.
103
+ def cache_key(query_string, variables)
104
+ payload = JSON.generate([query_string.gsub(/\s+/, ' ').strip, variables])
105
+ "printavo:gql:#{Digest::SHA256.hexdigest(payload)[0, 16]}"
106
+ end
107
+
89
108
  def execute(document, variables: {})
90
109
  response = @connection.post('') do |req|
91
110
  req.body = JSON.generate(query: document, variables: variables)
@@ -0,0 +1,76 @@
1
+ # lib/printavo/memory_store.rb
2
+ # frozen_string_literal: true
3
+
4
+ module Printavo
5
+ # A simple thread-safe in-memory cache store for use without Rails or Redis.
6
+ # Implements the same +fetch+ / +delete+ interface as +Rails.cache+, so it
7
+ # can be swapped for any compatible store without changing call sites.
8
+ #
9
+ # @example Standalone use
10
+ # client = Printavo::Client.new(
11
+ # email: ENV["PRINTAVO_EMAIL"],
12
+ # token: ENV["PRINTAVO_TOKEN"],
13
+ # cache: Printavo::MemoryStore.new
14
+ # )
15
+ #
16
+ # @example With custom default TTL
17
+ # client = Printavo::Client.new(
18
+ # email: ENV["PRINTAVO_EMAIL"],
19
+ # token: ENV["PRINTAVO_TOKEN"],
20
+ # cache: Printavo::MemoryStore.new,
21
+ # default_ttl: 600 # 10 minutes
22
+ # )
23
+ class MemoryStore
24
+ def initialize
25
+ @store = {}
26
+ @expires_at = {}
27
+ @mutex = Mutex.new
28
+ end
29
+
30
+ # Returns the cached value for +key+, or calls the block, stores the
31
+ # result, and returns it. Expired entries are treated as missing.
32
+ #
33
+ # @param key [String]
34
+ # @param expires_in [Integer, nil] TTL in seconds; +nil+ means no expiry
35
+ # @yieldreturn the value to cache on a miss
36
+ # @return the cached or freshly-computed value
37
+ def fetch(key, expires_in: nil)
38
+ @mutex.synchronize do
39
+ cached = read(key)
40
+ return cached unless cached.nil?
41
+
42
+ yield.tap { |v| write(key, v, expires_in: expires_in) }
43
+ end
44
+ end
45
+
46
+ # Removes +key+ from the cache.
47
+ #
48
+ # @param key [String]
49
+ # @return [nil]
50
+ def delete(key)
51
+ @mutex.synchronize do
52
+ @store.delete(key)
53
+ @expires_at.delete(key)
54
+ end
55
+ nil
56
+ end
57
+
58
+ private
59
+
60
+ def read(key)
61
+ exp = @expires_at[key]
62
+ if @store.key?(key) && (exp.nil? || Time.now < exp)
63
+ @store[key]
64
+ else
65
+ @store.delete(key)
66
+ @expires_at.delete(key)
67
+ nil
68
+ end
69
+ end
70
+
71
+ def write(key, value, expires_in: nil)
72
+ @store[key] = value
73
+ @expires_at[key] = Time.now + expires_in if expires_in
74
+ end
75
+ end
76
+ end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Printavo
5
- VERSION = '0.17.0'
5
+ VERSION = '0.18.0'
6
6
  end
data/lib/printavo.rb CHANGED
@@ -11,6 +11,7 @@ require_relative 'printavo/connection'
11
11
  require_relative 'printavo/enums'
12
12
  require_relative 'printavo/errors'
13
13
  require_relative 'printavo/graphql_client'
14
+ require_relative 'printavo/memory_store'
14
15
  require_relative 'printavo/page'
15
16
  require_relative 'printavo/models/base'
16
17
  require_relative 'printavo/models/account'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: printavo-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stan Carver II
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-01 00:00:00.000000000 Z
11
+ date: 2026-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -465,6 +465,7 @@ files:
465
465
  - lib/printavo/graphql/vendors/all.graphql
466
466
  - lib/printavo/graphql/vendors/find.graphql
467
467
  - lib/printavo/graphql_client.rb
468
+ - lib/printavo/memory_store.rb
468
469
  - lib/printavo/models/account.rb
469
470
  - lib/printavo/models/approval_request.rb
470
471
  - lib/printavo/models/base.rb