payload-api 0.4.1 → 0.6.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +2 -0
  3. data/LICENSE +1 -1
  4. data/README.md +23 -2
  5. data/lib/payload/arm/attr.rb +169 -0
  6. data/lib/payload/arm/object.rb +44 -1
  7. data/lib/payload/arm/request.rb +66 -6
  8. data/lib/payload/arm/session.rb +13 -9
  9. data/lib/payload/exceptions.rb +15 -0
  10. data/lib/payload/objects.rb +81 -18
  11. data/lib/payload/version.rb +2 -2
  12. data/lib/payload.rb +15 -5
  13. data/spec/objects/v1/access_token_spec.rb +19 -0
  14. data/spec/objects/v1/account_spec.rb +97 -0
  15. data/spec/objects/v1/billing_spec.rb +54 -0
  16. data/spec/objects/v1/invoice_spec.rb +53 -0
  17. data/spec/objects/v1/payment_link_spec.rb +50 -0
  18. data/spec/objects/v1/payment_method_spec.rb +106 -0
  19. data/spec/objects/{payment_spec.rb → v1/payment_spec.rb} +5 -6
  20. data/spec/objects/v1/session_spec.rb +89 -0
  21. data/spec/objects/v1/transaction_spec.rb +55 -0
  22. data/spec/objects/v2/account_spec.rb +211 -0
  23. data/spec/objects/v2/invoice_spec.rb +53 -0
  24. data/spec/objects/v2/payment_method_spec.rb +106 -0
  25. data/spec/objects/v2/transaction_spec.rb +48 -0
  26. data/spec/payload/arm/arm_request_query_spec.rb +226 -0
  27. data/spec/payload/arm/attr_spec.rb +216 -0
  28. data/spec/payload/arm/object_spec.rb +114 -0
  29. data/spec/payload/arm/request_format_integration_spec.rb +166 -0
  30. data/spec/payload/arm/request_spec.rb +259 -1
  31. data/spec/payload/arm/session_spec.rb +40 -0
  32. data/spec/payload/exceptions_spec.rb +82 -0
  33. data/spec/support/helpers/v1_helpers.rb +159 -0
  34. data/spec/support/helpers/v2_helpers.rb +205 -0
  35. data/spec/support/helpers.rb +15 -0
  36. data/spec/support/helpers_spec.rb +21 -0
  37. metadata +28 -6
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "payload"
4
+ require "payload/arm/attr"
5
+
6
+ RSpec.describe Payload::AttrRoot do
7
+ let(:root) { described_class.new }
8
+
9
+ describe "attribute access" do
10
+ it "returns an Attr for any method name" do
11
+ expect(root.id).to be_a(Payload::Attr)
12
+ expect(root.id.to_s).to eq("id")
13
+ expect(root.created_at).to be_a(Payload::Attr)
14
+ expect(root.created_at.to_s).to eq("created_at")
15
+ end
16
+
17
+ it "returns an Attr for chained property access (non-callable names stay as nested key)" do
18
+ chained = root.sender.account_id
19
+ expect(chained).to be_a(Payload::Attr)
20
+ expect(chained.to_s).to eq("sender[account_id]")
21
+ end
22
+
23
+ it "treats no-arg access as nested attribute" do
24
+ expect(root.created_at.month.to_s).to eq("created_at[month]")
25
+ expect(root.created_at.year.to_s).to eq("created_at[year]")
26
+ end
27
+
28
+ it "supports pl.attr.created_at(:month) style (function as symbol arg)" do
29
+ expect(root.created_at(:month).to_s).to eq("month(created_at)")
30
+ expect(root.created_at(:year).to_s).to eq("year(created_at)")
31
+ expect(root.amount(:sum).to_s).to eq("sum(amount)")
32
+ end
33
+
34
+ it "supports nested attr with symbol: pl.attr.totals.total(:sum) => sum(totals[total])" do
35
+ expect(root.totals.total(:sum).to_s).to eq("sum(totals[total])")
36
+ end
37
+ end
38
+ end
39
+
40
+ RSpec.describe Payload::Attr do
41
+ describe "simple attribute" do
42
+ it "has key and to_s as the param when no parent" do
43
+ attr = Payload::Attr.new("amount")
44
+ expect(attr.key).to eq("amount")
45
+ expect(attr.to_s).to eq("amount")
46
+ end
47
+ end
48
+
49
+ describe "nested attribute" do
50
+ it "builds key and to_s as parent[param]" do
51
+ parent = Payload::Attr.new("totals")
52
+ attr = Payload::Attr.new("total", parent)
53
+ expect(attr.key).to eq("totals[total]")
54
+ expect(attr.to_s).to eq("totals[total]")
55
+ end
56
+ end
57
+
58
+ describe "function form (after .call)" do
59
+ it "serializes as name(parent_key) when marked as method" do
60
+ parent = Payload::Attr.new("created_at")
61
+ attr = Payload::Attr.new("month", parent)
62
+ attr.call
63
+ expect(attr.to_s).to eq("month(created_at)")
64
+ end
65
+
66
+ it "serializes nested path in function form" do
67
+ parent = Payload::Attr.new("total", Payload::Attr.new("totals"))
68
+ attr = Payload::Attr.new("sum", parent)
69
+ attr.call
70
+ expect(attr.to_s).to eq("sum(totals[total])")
71
+ end
72
+
73
+ it "raises when chaining after a method" do
74
+ parent = Payload::Attr.new("created_at")
75
+ attr = Payload::Attr.new("month", parent)
76
+ attr.call
77
+ expect { attr.day }.to raise_error(RuntimeError, /cannot get attr of method/)
78
+ end
79
+ end
80
+
81
+ describe "#strip" do
82
+ it "returns to_s.strip so Attr works with request.select(*args, **data) and args.map(&:strip)" do
83
+ inner = Payload::Attr.new("created_at")
84
+ year_attr = Payload::Attr.new("year", inner)
85
+ year_attr.call
86
+ expect(year_attr.strip).to eq("year(created_at)")
87
+ end
88
+ end
89
+
90
+ describe "comparisons (return ARMFilter subclasses)" do
91
+ let(:attr_amount) { Payload::Attr.new("amount") }
92
+ let(:attr_status) { Payload::Attr.new("status") }
93
+
94
+ it "== returns ARMEqual with correct attr and opval" do
95
+ f = attr_status == "processed"
96
+ expect(f).to be_a(Payload::ARMEqual)
97
+ expect(f.attr).to eq("status")
98
+ expect(f.opval).to eq("processed")
99
+ end
100
+
101
+ it "!= returns ARMNotEqual with ! prefix" do
102
+ f = attr_status != "draft"
103
+ expect(f).to be_a(Payload::ARMNotEqual)
104
+ expect(f.attr).to eq("status")
105
+ expect(f.opval).to eq("!draft")
106
+ end
107
+
108
+ it "> returns ARMGreaterThan" do
109
+ f = attr_amount > 100
110
+ expect(f).to be_a(Payload::ARMGreaterThan)
111
+ expect(f.attr).to eq("amount")
112
+ expect(f.opval).to eq(">100")
113
+ end
114
+
115
+ it "< returns ARMLessThan" do
116
+ f = attr_amount < 500
117
+ expect(f).to be_a(Payload::ARMLessThan)
118
+ expect(f.opval).to eq("<500")
119
+ end
120
+
121
+ it ">= returns ARMGreaterThanEqual" do
122
+ f = attr_amount >= 100
123
+ expect(f).to be_a(Payload::ARMGreaterThanEqual)
124
+ expect(f.opval).to eq(">=100")
125
+ end
126
+
127
+ it "<= returns ARMLessThanEqual" do
128
+ f = attr_amount <= 100
129
+ expect(f).to be_a(Payload::ARMLessThanEqual)
130
+ expect(f.opval).to eq("<=100")
131
+ end
132
+
133
+ it "contains returns ARMContains with ?* op" do
134
+ attr = Payload::Attr.new("description")
135
+ f = attr.contains("INV -")
136
+ expect(f).to be_a(Payload::ARMContains)
137
+ expect(f.attr).to eq("description")
138
+ expect(f.opval).to eq("?*INV -")
139
+ end
140
+
141
+ it "comparisons use Attr key for nested paths" do
142
+ nested = Payload::Attr.new("total", Payload::Attr.new("totals"))
143
+ f = nested > 0
144
+ expect(f.attr).to eq("totals[total]")
145
+ expect(f.opval).to eq(">0")
146
+ end
147
+ end
148
+
149
+ describe "Attr class-level (Payload::Attr.name)" do
150
+ it "returns Attr via method_missing" do
151
+ expect(Payload::Attr.created_at).to be_a(Payload::Attr)
152
+ expect(Payload::Attr.created_at.to_s).to eq("created_at")
153
+ end
154
+ end
155
+ end
156
+
157
+ RSpec.describe Payload::ARMFilter do
158
+ describe "#| (OR)" do
159
+ it "combines two filters on the same attribute and returns ARMEqual with joined opval" do
160
+ left = Payload::ARMGreaterThan.new(Payload::Attr.amount, 100)
161
+ right = Payload::ARMLessThan.new(Payload::Attr.amount, 50)
162
+ combined = left | right
163
+ expect(combined).to be_a(Payload::ARMEqual)
164
+ expect(combined.attr).to eq("amount")
165
+ expect(combined.opval).to eq(">100|<50")
166
+ end
167
+
168
+ it "raises TypeError when other is not an ARMFilter" do
169
+ f = Payload::ARMEqual.new(Payload::Attr.status, "active")
170
+ expect { f | "invalid" }.to raise_error(TypeError, /invalid type/)
171
+ end
172
+
173
+ it "raises ArgumentError when attributes differ" do
174
+ f1 = Payload::ARMEqual.new(Payload::Attr.status, "active")
175
+ f2 = Payload::ARMEqual.new(Payload::Attr.type, "payment")
176
+ expect { f1 | f2 }.to raise_error(ArgumentError, /only works on the same attribute/)
177
+ end
178
+ end
179
+
180
+ describe "filter subclasses op values" do
181
+ it "ARMEqual has empty op" do
182
+ f = Payload::ARMEqual.new("status", "active")
183
+ expect(f.opval).to eq("active")
184
+ end
185
+
186
+ it "ARMNotEqual has ! prefix" do
187
+ f = Payload::ARMNotEqual.new("status", "draft")
188
+ expect(f.opval).to eq("!draft")
189
+ end
190
+
191
+ it "ARMGreaterThan has > prefix" do
192
+ f = Payload::ARMGreaterThan.new("amount", 100)
193
+ expect(f.opval).to eq(">100")
194
+ end
195
+
196
+ it "ARMLessThan has < prefix" do
197
+ f = Payload::ARMLessThan.new("amount", 200)
198
+ expect(f.opval).to eq("<200")
199
+ end
200
+
201
+ it "ARMGreaterThanEqual has >= prefix" do
202
+ f = Payload::ARMGreaterThanEqual.new("amount", 100)
203
+ expect(f.opval).to eq(">=100")
204
+ end
205
+
206
+ it "ARMLessThanEqual has <= prefix" do
207
+ f = Payload::ARMLessThanEqual.new("amount", 100)
208
+ expect(f.opval).to eq("<=100")
209
+ end
210
+
211
+ it "ARMContains has ?* prefix" do
212
+ f = Payload::ARMContains.new("email", "example.com")
213
+ expect(f.opval).to eq("?*example.com")
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "payload"
4
+ require "payload/arm/object"
5
+
6
+ RSpec.describe Payload::ARMObject do
7
+ describe "method_missing (attribute access)" do
8
+ let(:session) { Payload::Session.new("test_key", "https://api.test.com", "v2") }
9
+
10
+ it "returns value for present key" do
11
+ obj = Payload::Invoice.new({ "id" => "inv_1", "amount" => 100 }, session)
12
+ expect(obj.id).to eq("inv_1")
13
+ expect(obj.amount).to eq(100)
14
+ end
15
+
16
+ it "raises NoMethodError for missing key (strict behavior preserved)" do
17
+ obj = Payload::Invoice.new({ "id" => "inv_1" }, session)
18
+ expect { obj.nonexistent_key }.to raise_error(NoMethodError, /nonexistent_key/)
19
+ expect { obj.amount }.to raise_error(NoMethodError, /amount/)
20
+ end
21
+
22
+ it "strips trailing = for setter-like names and returns value" do
23
+ obj = Payload::Account.new({ "name" => "Acme" }, session)
24
+ expect(obj.name).to eq("Acme")
25
+ end
26
+ end
27
+
28
+ describe "#json" do
29
+ let(:session) { Payload::Session.new("test_key", "https://api.test.com", "v2") }
30
+
31
+ it "returns same as to_json" do
32
+ obj = Payload::Invoice.new({ "id" => "inv_1", "object" => "invoice" }, session)
33
+ expect(obj.json).to eq(obj.to_json)
34
+ expect(obj.json).to include("inv_1")
35
+ expect(obj.json).to include("invoice")
36
+ end
37
+ end
38
+
39
+ describe ".order_by, .limit, .offset" do
40
+ it "delegates to ARMRequest and returns chainable request" do
41
+ req = Payload::Invoice.order_by("created_at")
42
+ expect(req).to be_a(Payload::ARMRequest)
43
+ expect(req.instance_variable_get(:@order_by)).to include("created_at")
44
+
45
+ req = Payload::Invoice.limit(10)
46
+ expect(req.instance_variable_get(:@limit)).to eq(10)
47
+
48
+ req = Payload::Invoice.offset(20)
49
+ expect(req.instance_variable_get(:@offset)).to eq(20)
50
+ end
51
+ end
52
+
53
+ describe ".select" do
54
+ it "delegates to ARMRequest and sets fields filter" do
55
+ req = Payload::Invoice.select("id", "amount")
56
+ expect(req).to be_a(Payload::ARMRequest)
57
+ expect(req.instance_variable_get(:@filters)["fields"]).to eq("id,amount")
58
+ end
59
+ end
60
+
61
+ describe "#[] (bracket access)" do
62
+ let(:session) { Payload::Session.new("test_key", "https://api.test.com", "v2") }
63
+
64
+ it "returns value for string key" do
65
+ obj = Payload::Invoice.new({ "id" => "inv_1", "status" => "open" }, session)
66
+ expect(obj["id"]).to eq("inv_1")
67
+ expect(obj["status"]).to eq("open")
68
+ end
69
+
70
+ it "returns value for symbol key (data keys are stored as strings)" do
71
+ obj = Payload::Invoice.new({ "id" => "inv_1" }, session)
72
+ expect(obj[:id]).to eq("inv_1")
73
+ end
74
+
75
+ it "returns nil for missing key" do
76
+ obj = Payload::Invoice.new({ "id" => "inv_1" }, session)
77
+ expect(obj["missing"]).to be_nil
78
+ end
79
+ end
80
+
81
+ describe "#to_json" do
82
+ let(:session) { Payload::Session.new("test_key", "https://api.test.com", "v2") }
83
+
84
+ it "includes @data in JSON output" do
85
+ obj = Payload::Invoice.new({ "id" => "inv_1", "object" => "invoice", "amount" => 99 }, session)
86
+ json = obj.to_json
87
+ expect(json).to include("inv_1")
88
+ expect(json).to include("invoice")
89
+ expect(json).to include("99")
90
+ end
91
+
92
+ it "merges class poly when present" do
93
+ obj = Payload::Payment.new({ "id" => "txn_1", "object" => "transaction", "type" => "payment" }, session)
94
+ json = obj.to_json
95
+ expect(json).to include("payment")
96
+ expect(json).to include("txn_1")
97
+ end
98
+ end
99
+
100
+ describe "#respond_to_missing?" do
101
+ let(:session) { Payload::Session.new("test_key", "https://api.test.com", "v2") }
102
+
103
+ it "returns true for key present in data" do
104
+ obj = Payload::Invoice.new({ "id" => "inv_1", "amount" => 100 }, session)
105
+ expect(obj.respond_to?(:id)).to be true
106
+ expect(obj.respond_to?(:amount)).to be true
107
+ end
108
+
109
+ it "returns false for key missing from data (so method_missing will call super)" do
110
+ obj = Payload::Invoice.new({ "id" => "inv_1" }, session)
111
+ expect(obj.respond_to?(:nonexistent_key)).to be false
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "payload"
4
+ require "payload/arm/object"
5
+ require_relative "../../support/helpers"
6
+
7
+ RSpec.describe "confirm request format is valid" do
8
+ include_context "test helpers"
9
+
10
+ [1, 2].each do |api_version|
11
+ context "API v#{api_version}" do
12
+ let(:session) { Payload::Session.new(Payload.api_key, Payload.api_url, api_version) }
13
+ let(:pl) { session }
14
+
15
+ context "date functions" do
16
+ it "filter with year(attr) ==" do
17
+ results = pl.Transaction.filter_by(
18
+ pl.attr.type == "payment",
19
+ pl.attr.created_at(:year) == Time.now.year
20
+ ).limit(5).all
21
+ expect(results).to be_an(Array)
22
+ end
23
+
24
+ it "filter with month(attr) ==" do
25
+ results = pl.Transaction.filter_by(
26
+ pl.attr.created_at(:month) == Time.now.month
27
+ ).limit(5).all
28
+ expect(results).to be_an(Array)
29
+ end
30
+
31
+ it "select with dayname(attr)" do
32
+ results = pl.Transaction.select(
33
+ pl.attr.id,
34
+ pl.attr.created_at(:dayname)
35
+ ).limit(5).all
36
+ expect(results).to be_an(Array)
37
+ end
38
+
39
+ it "group_by year(attr) with count" do
40
+ yearly = pl.Transaction.select(
41
+ pl.attr.created_at(:year),
42
+ pl.attr.id(:count)
43
+ ).group_by(pl.attr.created_at(:year)).limit(5).all
44
+ expect(yearly).to be_an(Array)
45
+ end
46
+ end
47
+
48
+ context "order_by" do
49
+ it "order_by ascending" do
50
+ results = pl.Transaction.select(pl.attr.id).order_by(pl.attr.created_at).limit(5).all
51
+ expect(results).to be_an(Array)
52
+ end
53
+
54
+ it "order_by descending" do
55
+ results = pl.Transaction.select(pl.attr.id).order_by(pl.attr.created_at(:desc)).limit(5).all
56
+ expect(results).to be_an(Array)
57
+ end
58
+ end
59
+
60
+ context "limit, offset, and range" do
61
+ it "limit" do
62
+ results = pl.Transaction.select(pl.attr.id).limit(2).all
63
+ expect(results).to be_an(Array)
64
+ expect(results.length).to be <= 2
65
+ end
66
+
67
+ it "offset" do
68
+ page1 = pl.Transaction.select(pl.attr.id).limit(3).offset(0).all
69
+ page2 = pl.Transaction.select(pl.attr.id).limit(3).offset(3).all
70
+ expect(page1).to be_an(Array)
71
+ expect(page2).to be_an(Array)
72
+ expect(page1.length).to be <= 3
73
+ expect(page2.length).to be <= 3
74
+ end
75
+
76
+ it "range operator []" do
77
+ results = pl.Transaction.select(pl.attr.id)[0..2]
78
+ expect(results).to be_an(Array)
79
+ expect(results.length).to be <= 3
80
+ end
81
+ end
82
+
83
+ context "nested attr" do
84
+ it "select with nested attr (sender.account_id)" do
85
+ results = pl.Transaction.select(
86
+ pl.attr.id,
87
+ pl.attr.sender.account_id
88
+ ).filter_by(pl.attr.type == "payment").limit(5).all
89
+ expect(results).to be_an(Array)
90
+ end
91
+ end
92
+
93
+ context "filter operators" do
94
+ it "filter with >" do
95
+ results = pl.Transaction.filter_by(
96
+ pl.attr.type == "payment",
97
+ pl.attr.amount > 0
98
+ ).select(pl.attr.id, pl.attr.amount).limit(5).all
99
+ expect(results).to be_an(Array)
100
+ end
101
+
102
+ it "filter with <" do
103
+ results = pl.Transaction.filter_by(
104
+ pl.attr.type == "payment",
105
+ pl.attr.amount < 1_000_000
106
+ ).select(pl.attr.id, pl.attr.amount).limit(5).all
107
+ expect(results).to be_an(Array)
108
+ end
109
+
110
+ it "filter with !=" do
111
+ results = pl.Transaction.filter_by(
112
+ pl.attr.type != "nonexistent_type"
113
+ ).select(pl.attr.id, pl.attr.type).limit(5).all
114
+ expect(results).to be_an(Array)
115
+ end
116
+
117
+ it "filter with contains" do
118
+ results = pl.Transaction.filter_by(
119
+ pl.attr.type == "payment",
120
+ pl.attr.description.contains("")
121
+ ).select(pl.attr.id, pl.attr.description).limit(5).all
122
+ expect(results).to be_an(Array)
123
+ end
124
+ end
125
+
126
+ context "filter OR (chained conditions)" do
127
+ it "filter with OR (|) on same attribute" do
128
+ or_filter = (pl.attr.amount > 0) | (pl.attr.amount < 1_000_000)
129
+ results = pl.Transaction.filter_by(
130
+ pl.attr.type == "payment",
131
+ or_filter
132
+ ).select(pl.attr.id, pl.attr.amount).limit(5).all
133
+ expect(results).to be_an(Array)
134
+ end
135
+ end
136
+
137
+ context "group_by and aggregates" do
138
+ it "group_by with sum and count" do
139
+ results = pl.Transaction.select(
140
+ pl.attr.type,
141
+ pl.attr.amount(:sum),
142
+ pl.attr.id(:count)
143
+ ).filter_by(pl.attr.type == "payment").group_by(pl.attr.type).limit(5).all
144
+ expect(results).to be_an(Array)
145
+ end
146
+
147
+ it "group_by month(attr) with sum and count" do
148
+ results = pl.Transaction.select(
149
+ pl.attr.created_at(:month),
150
+ pl.attr.amount(:sum),
151
+ pl.attr.id(:count)
152
+ ).filter_by(pl.attr.type == "payment").group_by(pl.attr.created_at(:month)).limit(5).all
153
+ expect(results).to be_an(Array)
154
+ end
155
+
156
+ it "group_by year(attr) with count" do
157
+ results = pl.Transaction.select(
158
+ pl.attr.created_at(:year),
159
+ pl.attr.id(:count)
160
+ ).filter_by(pl.attr.type == "payment").group_by(pl.attr.created_at(:year)).limit(5).all
161
+ expect(results).to be_an(Array)
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end