Skip to content

Commit 3d22b1a

Browse files
author
Wiktor Plaga
authored
Add JsonImport resource for bulk data import (PIP-310) (#137)
1 parent c9878e8 commit 3d22b1a

3 files changed

Lines changed: 313 additions & 0 deletions

File tree

chartmogul/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from .api.account import Account
3030
from .api.activity import Activity
3131
from .api.activities_export import ActivitiesExport
32+
from .api.json_import import JsonImport
3233

3334
from .version import __version__
3435

chartmogul/api/json_import.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from marshmallow import Schema, fields, post_load, EXCLUDE
2+
from ..resource import Resource
3+
from chartmogul import ArgumentMissingError
4+
5+
6+
class JsonImport(Resource):
7+
"""
8+
https://dev.chartmogul.com/reference/bulk-import/
9+
"""
10+
11+
_path = "/data_sources{/data_source_uuid}/json_imports{/id}"
12+
13+
class _Schema(Schema):
14+
id = fields.String()
15+
data_source_uuid = fields.String(allow_none=True)
16+
status = fields.String(allow_none=True)
17+
external_id = fields.String(allow_none=True)
18+
status_details = fields.Raw(allow_none=True)
19+
created_at = fields.DateTime(allow_none=True)
20+
updated_at = fields.DateTime(allow_none=True)
21+
22+
@post_load
23+
def make(self, data, **kwargs):
24+
return JsonImport(**data)
25+
26+
_schema = _Schema(unknown=EXCLUDE)
27+
28+
@classmethod
29+
def _validate_arguments(cls, method, kwargs):
30+
if method in ["create", "retrieve"] and "data_source_uuid" not in kwargs:
31+
raise ArgumentMissingError("Please pass 'data_source_uuid' parameter")
32+
if method in ["create"] and "data" not in kwargs:
33+
raise ArgumentMissingError("Please pass 'data' parameter")
34+
if method in ["retrieve"] and "id" not in kwargs:
35+
raise ArgumentMissingError("Please pass 'id' parameter")
36+
37+
38+
JsonImport.create = JsonImport._method(
39+
"create", "post", "/data_sources{/data_source_uuid}/json_imports")
40+
JsonImport.retrieve = JsonImport._method(
41+
"retrieve", "get", "/data_sources{/data_source_uuid}/json_imports{/id}")

test/api/test_json_import.py

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import unittest
2+
3+
import requests_mock
4+
5+
from chartmogul import JsonImport, Config
6+
7+
8+
importRequestData = {
9+
"external_id": "import_batch_2026_04",
10+
"customers": [
11+
{
12+
"external_id": "cus_acme_001",
13+
"name": "Acme Corp",
14+
"email": "billing@acme.com",
15+
"company": "Acme Corporation",
16+
"country": "US",
17+
"state": "US-CA",
18+
"city": "San Francisco",
19+
"zip": "94105",
20+
"lead_created_at": "2025-10-15T00:00:00Z",
21+
"free_trial_started_at": "2025-11-01T00:00:00Z",
22+
},
23+
{
24+
"external_id": "cus_globex_002",
25+
"name": "Globex Inc",
26+
"email": "accounts@globex.io",
27+
"company": "Globex Inc",
28+
"country": "DE",
29+
"city": "Berlin",
30+
},
31+
],
32+
"plans": [
33+
{
34+
"name": "Professional Monthly",
35+
"external_id": "plan_pro_monthly",
36+
"interval_count": 1,
37+
"interval_unit": "month",
38+
},
39+
{
40+
"name": "Enterprise Annual",
41+
"external_id": "plan_ent_annual",
42+
"interval_count": 1,
43+
"interval_unit": "year",
44+
},
45+
],
46+
"invoices": [
47+
{
48+
"external_id": "inv_2025_11_001",
49+
"customer_external_id": "cus_acme_001",
50+
"date": "2025-11-01T00:00:00Z",
51+
"due_date": "2025-12-01T00:00:00Z",
52+
"currency": "USD",
53+
"collection_method": "automatic",
54+
},
55+
{
56+
"external_id": "inv_2025_12_001",
57+
"customer_external_id": "cus_acme_001",
58+
"date": "2025-12-01T00:00:00Z",
59+
"due_date": "2026-01-01T00:00:00Z",
60+
"currency": "USD",
61+
},
62+
{
63+
"external_id": "inv_2025_11_002",
64+
"customer_external_id": "cus_globex_002",
65+
"date": "2025-11-15T00:00:00Z",
66+
"due_date": "2025-12-15T00:00:00Z",
67+
"currency": "EUR",
68+
"collection_method": "manual",
69+
},
70+
],
71+
"line_items": [
72+
{
73+
"invoice_external_id": "inv_2025_11_001",
74+
"type": "subscription",
75+
"amount_in_cents": 9900,
76+
"quantity": 5,
77+
"plan_external_id": "plan_pro_monthly",
78+
"subscription_external_id": "sub_acme_pro",
79+
"service_period_start": "2025-11-01T00:00:00Z",
80+
"service_period_end": "2025-12-01T00:00:00Z",
81+
},
82+
{
83+
"invoice_external_id": "inv_2025_12_001",
84+
"type": "subscription",
85+
"amount_in_cents": 9900,
86+
"quantity": 5,
87+
"plan_external_id": "plan_pro_monthly",
88+
"subscription_external_id": "sub_acme_pro",
89+
"service_period_start": "2025-12-01T00:00:00Z",
90+
"service_period_end": "2026-01-01T00:00:00Z",
91+
"discount_amount_in_cents": 500,
92+
"discount_code": "WINTER25",
93+
"tax_amount_in_cents": 1700,
94+
},
95+
{
96+
"invoice_external_id": "inv_2025_11_002",
97+
"type": "subscription",
98+
"amount_in_cents": 249900,
99+
"quantity": 1,
100+
"plan_external_id": "plan_ent_annual",
101+
"subscription_external_id": "sub_globex_ent",
102+
"service_period_start": "2025-11-15T00:00:00Z",
103+
"service_period_end": "2026-11-15T00:00:00Z",
104+
"tax_amount_in_cents": 47481,
105+
"transaction_fees_in_cents": 7497,
106+
"transaction_fees_currency": "EUR",
107+
},
108+
{
109+
"invoice_external_id": "inv_2025_11_001",
110+
"type": "one_time",
111+
"amount_in_cents": 5000,
112+
"description": "Onboarding setup fee",
113+
},
114+
],
115+
"transactions": [
116+
{
117+
"invoice_external_id": "inv_2025_11_001",
118+
"external_id": "txn_001",
119+
"type": "payment",
120+
"result": "successful",
121+
"date": "2025-11-01T12:30:00Z",
122+
},
123+
{
124+
"invoice_external_id": "inv_2025_12_001",
125+
"external_id": "txn_002",
126+
"type": "payment",
127+
"result": "successful",
128+
"date": "2025-12-01T08:15:00Z",
129+
},
130+
{
131+
"invoice_external_id": "inv_2025_11_002",
132+
"external_id": "txn_003",
133+
"type": "payment",
134+
"result": "successful",
135+
"date": "2025-11-16T10:00:00Z",
136+
"amount_in_cents": 249900,
137+
},
138+
],
139+
"subscription_events": [
140+
{
141+
"external_id": "evt_acme_start",
142+
"customer_external_id": "cus_acme_001",
143+
"subscription_external_id": "sub_acme_pro",
144+
"plan_external_id": "plan_pro_monthly",
145+
"event_type": "subscription_start",
146+
"event_date": "2025-11-01T00:00:00Z",
147+
"effective_date": "2025-11-01T00:00:00Z",
148+
"currency": "USD",
149+
"amount_in_cents": 9900,
150+
"quantity": 5,
151+
},
152+
{
153+
"external_id": "evt_globex_start",
154+
"customer_external_id": "cus_globex_002",
155+
"subscription_external_id": "sub_globex_ent",
156+
"subscription_set_external_id": "set_globex_main",
157+
"plan_external_id": "plan_ent_annual",
158+
"event_type": "subscription_start",
159+
"event_date": "2025-11-15T00:00:00Z",
160+
"effective_date": "2025-11-15T00:00:00Z",
161+
"currency": "EUR",
162+
"amount_in_cents": 249900,
163+
"quantity": 1,
164+
"tax_amount_in_cents": 47481,
165+
},
166+
],
167+
}
168+
169+
importResponseData = {
170+
"id": "4815d987-abcd-11ee-a987-978df45c5114",
171+
"data_source_uuid": "ds_45d064ca-fcf8-11f0-903f-33618f80d753",
172+
"status": "queued",
173+
"external_id": "import_batch_2026_04",
174+
"status_details": {},
175+
"created_at": "2026-04-14T10:30:00Z",
176+
"updated_at": "2026-04-14T10:30:00Z",
177+
}
178+
179+
importStatusResponseData = {
180+
"id": "4815d987-abcd-11ee-a987-978df45c5114",
181+
"data_source_uuid": "ds_45d064ca-fcf8-11f0-903f-33618f80d753",
182+
"status": "completed",
183+
"external_id": "import_batch_2026_04",
184+
"status_details": {
185+
"plans": {"status": "imported"},
186+
"cus_acme_001": {
187+
"status": "imported",
188+
"invoices": {"status": "imported"},
189+
"customers": {"status": "imported"},
190+
"line_items": {"status": "imported"},
191+
"transactions": {"status": "imported"},
192+
"subscription_events": {"status": "imported"},
193+
},
194+
"cus_globex_002": {
195+
"status": "imported",
196+
"invoices": {"status": "imported"},
197+
"customers": {"status": "imported"},
198+
"line_items": {"status": "imported"},
199+
"transactions": {"status": "imported"},
200+
"subscription_events": {"status": "imported"},
201+
},
202+
},
203+
"created_at": "2026-04-14T10:30:00Z",
204+
"updated_at": "2026-04-14T10:31:15Z",
205+
}
206+
207+
208+
class JsonImportTestCase(unittest.TestCase):
209+
210+
@requests_mock.mock()
211+
def test_create(self, mock_requests):
212+
mock_requests.register_uri(
213+
"POST",
214+
"https://api.chartmogul.com/v1/data_sources/ds_45d064ca-fcf8-11f0-903f-33618f80d753/json_imports",
215+
request_headers={"Authorization": "Basic dG9rZW46"},
216+
status_code=200,
217+
json=importResponseData,
218+
)
219+
220+
config = Config("token")
221+
result = JsonImport.create(
222+
config,
223+
data_source_uuid="ds_45d064ca-fcf8-11f0-903f-33618f80d753",
224+
data=importRequestData,
225+
).get()
226+
227+
self.assertEqual(mock_requests.call_count, 1)
228+
sent = mock_requests.last_request.json()
229+
self.assertEqual(sent["external_id"], "import_batch_2026_04")
230+
self.assertEqual(len(sent["customers"]), 2)
231+
self.assertEqual(sent["customers"][0]["external_id"], "cus_acme_001")
232+
self.assertEqual(len(sent["plans"]), 2)
233+
self.assertEqual(sent["plans"][0]["interval_unit"], "month")
234+
self.assertEqual(len(sent["invoices"]), 3)
235+
self.assertEqual(len(sent["line_items"]), 4)
236+
self.assertEqual(len(sent["transactions"]), 3)
237+
self.assertEqual(len(sent["subscription_events"]), 2)
238+
239+
self.assertTrue(isinstance(result, JsonImport))
240+
self.assertEqual(result.id, "4815d987-abcd-11ee-a987-978df45c5114")
241+
self.assertEqual(result.data_source_uuid, "ds_45d064ca-fcf8-11f0-903f-33618f80d753")
242+
self.assertEqual(result.status, "queued")
243+
self.assertEqual(result.external_id, "import_batch_2026_04")
244+
self.assertEqual(result.status_details, {})
245+
246+
@requests_mock.mock()
247+
def test_retrieve(self, mock_requests):
248+
mock_requests.register_uri(
249+
"GET",
250+
"https://api.chartmogul.com/v1/data_sources/ds_45d064ca-fcf8-11f0-903f-33618f80d753/json_imports/4815d987-abcd-11ee-a987-978df45c5114",
251+
request_headers={"Authorization": "Basic dG9rZW46"},
252+
status_code=200,
253+
json=importStatusResponseData,
254+
)
255+
256+
config = Config("token")
257+
result = JsonImport.retrieve(
258+
config,
259+
data_source_uuid="ds_45d064ca-fcf8-11f0-903f-33618f80d753",
260+
id="4815d987-abcd-11ee-a987-978df45c5114",
261+
).get()
262+
263+
self.assertEqual(mock_requests.call_count, 1)
264+
self.assertTrue(isinstance(result, JsonImport))
265+
self.assertEqual(result.id, "4815d987-abcd-11ee-a987-978df45c5114")
266+
self.assertEqual(result.status, "completed")
267+
self.assertEqual(result.external_id, "import_batch_2026_04")
268+
self.assertEqual(result.status_details["plans"]["status"], "imported")
269+
self.assertEqual(
270+
result.status_details["cus_acme_001"]["invoices"]["status"], "imported"
271+
)

0 commit comments

Comments
 (0)