speedy-bg 0.1.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 +7 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +67 -0
- data/LICENSE.txt +21 -0
- data/README.md +150 -0
- data/SECURITY.md +29 -0
- data/docs/API.md +740 -0
- data/docs/RESPONSE_CONTRACTS.md +55 -0
- data/docs/VCR.md +161 -0
- data/docs/schema-notes.md +38 -0
- data/lib/speedy/camel_case_middleware.rb +71 -0
- data/lib/speedy/client.rb +100 -0
- data/lib/speedy/client_resources.rb +776 -0
- data/lib/speedy/collection.rb +58 -0
- data/lib/speedy/error.rb +29 -0
- data/lib/speedy/model.rb +92 -0
- data/lib/speedy/models/calculation/address_location.rb +16 -0
- data/lib/speedy/models/calculation/calculation_content.rb +15 -0
- data/lib/speedy/models/calculation/calculation_recipient.rb +15 -0
- data/lib/speedy/models/calculation/calculation_result.rb +19 -0
- data/lib/speedy/models/calculation/calculation_sender.rb +16 -0
- data/lib/speedy/models/calculation/calculation_service.rb +16 -0
- data/lib/speedy/models/clients/client.rb +21 -0
- data/lib/speedy/models/clients/cod_contract_info.rb +16 -0
- data/lib/speedy/models/clients/cod_international_annex.rb +12 -0
- data/lib/speedy/models/clients/contract_info.rb +16 -0
- data/lib/speedy/models/clients/requirement.rb +12 -0
- data/lib/speedy/models/clients/special_delivery_requirements.rb +12 -0
- data/lib/speedy/models/common/address.rb +36 -0
- data/lib/speedy/models/error.rb +37 -0
- data/lib/speedy/models/location/address_nomenclature_type.rb +12 -0
- data/lib/speedy/models/location/block.rb +15 -0
- data/lib/speedy/models/location/complex.rb +23 -0
- data/lib/speedy/models/location/country.rb +25 -0
- data/lib/speedy/models/location/find_office_response.rb +12 -0
- data/lib/speedy/models/location/find_site_response.rb +12 -0
- data/lib/speedy/models/location/get_office_response.rb +12 -0
- data/lib/speedy/models/location/get_site_response.rb +12 -0
- data/lib/speedy/models/location/office.rb +41 -0
- data/lib/speedy/models/location/office_result.rb +13 -0
- data/lib/speedy/models/location/office_working_time_schedule.rb +15 -0
- data/lib/speedy/models/location/point_of_interest.rb +22 -0
- data/lib/speedy/models/location/postcode.rb +12 -0
- data/lib/speedy/models/location/routing_information.rb +14 -0
- data/lib/speedy/models/location/site.rb +30 -0
- data/lib/speedy/models/location/state.rb +17 -0
- data/lib/speedy/models/location/street.rb +23 -0
- data/lib/speedy/models/location/tour.rb +14 -0
- data/lib/speedy/models/payments/payout.rb +20 -0
- data/lib/speedy/models/payments/payout_details.rb +23 -0
- data/lib/speedy/models/pickup/pickup_order.rb +16 -0
- data/lib/speedy/models/pickup/terms_response.rb +12 -0
- data/lib/speedy/models/print/extended_print_response.rb +13 -0
- data/lib/speedy/models/print/label_info.rb +19 -0
- data/lib/speedy/models/print/label_info_response.rb +12 -0
- data/lib/speedy/models/print/parcel_to_print_additional_barcode.rb +14 -0
- data/lib/speedy/models/print/print_response.rb +12 -0
- data/lib/speedy/models/response.rb +13 -0
- data/lib/speedy/models/services/additional_courier_service.rb +11 -0
- data/lib/speedy/models/services/additional_courier_services.rb +21 -0
- data/lib/speedy/models/services/courier_service.rb +19 -0
- data/lib/speedy/models/services/extended_courier_service.rb +13 -0
- data/lib/speedy/models/shipments/bank_account.rb +12 -0
- data/lib/speedy/models/shipments/barcode_information.rb +20 -0
- data/lib/speedy/models/shipments/cod_payment.rb +12 -0
- data/lib/speedy/models/shipments/content.rb +20 -0
- data/lib/speedy/models/shipments/create_shipment_response.rb +16 -0
- data/lib/speedy/models/shipments/created_shipment_parcel.rb +14 -0
- data/lib/speedy/models/shipments/external_carrier_parcel_number.rb +12 -0
- data/lib/speedy/models/shipments/goods_item.rb +16 -0
- data/lib/speedy/models/shipments/money_transfer_premium.rb +13 -0
- data/lib/speedy/models/shipments/parcel.rb +22 -0
- data/lib/speedy/models/shipments/payment.rb +16 -0
- data/lib/speedy/models/shipments/primary_shipment.rb +12 -0
- data/lib/speedy/models/shipments/recipient.rb +21 -0
- data/lib/speedy/models/shipments/return_amounts.rb +11 -0
- data/lib/speedy/models/shipments/secondary_shipment.rb +18 -0
- data/lib/speedy/models/shipments/sender.rb +21 -0
- data/lib/speedy/models/shipments/shipment.rb +28 -0
- data/lib/speedy/models/shipments/shipment_additional_services.rb +17 -0
- data/lib/speedy/models/shipments/shipment_address.rb +10 -0
- data/lib/speedy/models/shipments/shipment_cod_additional_service.rb +20 -0
- data/lib/speedy/models/shipments/shipment_cod_fiscal_receipt_item.rb +14 -0
- data/lib/speedy/models/shipments/shipment_content.rb +24 -0
- data/lib/speedy/models/shipments/shipment_declared_value_additional_service.rb +13 -0
- data/lib/speedy/models/shipments/shipment_delivery.rb +14 -0
- data/lib/speedy/models/shipments/shipment_discount_card_id.rb +12 -0
- data/lib/speedy/models/shipments/shipment_electronic_return_receipt_additional_service.rb +14 -0
- data/lib/speedy/models/shipments/shipment_obpd.rb +13 -0
- data/lib/speedy/models/shipments/shipment_parcel.rb +23 -0
- data/lib/speedy/models/shipments/shipment_parcel_number.rb +12 -0
- data/lib/speedy/models/shipments/shipment_parcel_ref.rb +13 -0
- data/lib/speedy/models/shipments/shipment_parcel_size.rb +13 -0
- data/lib/speedy/models/shipments/shipment_payment.rb +17 -0
- data/lib/speedy/models/shipments/shipment_phone_number.rb +12 -0
- data/lib/speedy/models/shipments/shipment_price.rb +23 -0
- data/lib/speedy/models/shipments/shipment_price_amount.rb +13 -0
- data/lib/speedy/models/shipments/shipment_recipient.rb +14 -0
- data/lib/speedy/models/shipments/shipment_return_additional_services.rb +20 -0
- data/lib/speedy/models/shipments/shipment_return_receipt_additional_service.rb +14 -0
- data/lib/speedy/models/shipments/shipment_return_voucher_additional_service.rb +14 -0
- data/lib/speedy/models/shipments/shipment_rod_additional_service.rb +15 -0
- data/lib/speedy/models/shipments/shipment_rop_additional_service.rb +12 -0
- data/lib/speedy/models/shipments/shipment_rop_additional_service_line.rb +12 -0
- data/lib/speedy/models/shipments/shipment_sender.rb +23 -0
- data/lib/speedy/models/shipments/shipment_service.rb +17 -0
- data/lib/speedy/models/shipments/shipment_swap_additional_service.rb +17 -0
- data/lib/speedy/models/shipments/update_shipment_response.rb +10 -0
- data/lib/speedy/models/tracking/bulk_tracking_data_file.rb +14 -0
- data/lib/speedy/models/tracking/external_carrier_parcel_number_details.rb +13 -0
- data/lib/speedy/models/tracking/tracked_parcel.rb +21 -0
- data/lib/speedy/models/tracking/tracked_parcel_operation.rb +22 -0
- data/lib/speedy/models/tracking/tracked_parcel_operation_additional_info.rb +17 -0
- data/lib/speedy/models/tracking/tracked_parcel_operation_additional_info_predict.rb +14 -0
- data/lib/speedy/models/tracking/tracked_parcel_operation_additional_info_recipient.rb +14 -0
- data/lib/speedy/models/validation/validation_response.rb +14 -0
- data/lib/speedy/resource_attributes.rb +87 -0
- data/lib/speedy/service_resource.rb +94 -0
- data/lib/speedy/services/calculation.rb +22 -0
- data/lib/speedy/services/clients.rb +40 -0
- data/lib/speedy/services/courier_services.rb +25 -0
- data/lib/speedy/services/location/block.rb +32 -0
- data/lib/speedy/services/location/complex.rb +39 -0
- data/lib/speedy/services/location/country.rb +70 -0
- data/lib/speedy/services/location/office.rb +50 -0
- data/lib/speedy/services/location/point_of_interest.rb +39 -0
- data/lib/speedy/services/location/postcode.rb +25 -0
- data/lib/speedy/services/location/site.rb +77 -0
- data/lib/speedy/services/location/state.rb +46 -0
- data/lib/speedy/services/location/street.rb +39 -0
- data/lib/speedy/services/location.rb +11 -0
- data/lib/speedy/services/payments.rb +19 -0
- data/lib/speedy/services/pickup.rb +38 -0
- data/lib/speedy/services/print.rb +58 -0
- data/lib/speedy/services/shipments/shipment.rb +120 -0
- data/lib/speedy/services/shipments.rb +11 -0
- data/lib/speedy/services/tracking.rb +26 -0
- data/lib/speedy/services/validation/address.rb +26 -0
- data/lib/speedy/services/validation/general.rb +37 -0
- data/lib/speedy/services/validation.rb +11 -0
- data/lib/speedy/transport_error.rb +17 -0
- data/lib/speedy/util.rb +67 -0
- data/lib/speedy/version.rb +5 -0
- data/lib/speedy-bg.rb +6 -0
- data/lib/speedy.rb +203 -0
- data/sig/speedy.rbs +374 -0
- metadata +227 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Offline response contracts
|
|
2
|
+
|
|
3
|
+
`spec/response_property_examples_spec.rb` exercises every endpoint shortcut in
|
|
4
|
+
`Speedy::ClientResources` through a real configured client and a local Faraday
|
|
5
|
+
test adapter. It makes no HTTP requests. A coverage assertion fails when a new
|
|
6
|
+
public shortcut lacks a scenario; the calculation alias is listed explicitly.
|
|
7
|
+
|
|
8
|
+
The checked-in `spec/fixtures/responses/*.json` files contain one synthetic
|
|
9
|
+
example per response type, assembled from the official schema snapshot. They exercise optional
|
|
10
|
+
properties as well as the required ones. They are examples of response shapes,
|
|
11
|
+
not successful live recordings or evidence that every optional property occurs
|
|
12
|
+
together. Tests never load the gitignored `schema/` directory. The authoritative
|
|
13
|
+
reference is the [Speedy API documentation](https://api.speedy.bg/api/docs/).
|
|
14
|
+
|
|
15
|
+
The recursive `expect_response_properties(actual, raw)` assertion derives a
|
|
16
|
+
snake_case accessor independently from each original JSON key. It calls the
|
|
17
|
+
accessor and compares every leaf with the original response value, including
|
|
18
|
+
false, zero, explicit null, nested models and every array item. The three
|
|
19
|
+
explicit dictionary structures (`details`, `detailsLocal`, and
|
|
20
|
+
`externalCarrierParcelNumbersDetails`) use hash-key lookup, not dot readers;
|
|
21
|
+
dictionaries retain the client's existing snake_case normalization
|
|
22
|
+
of keys, and typed dictionary values are checked using their model accessors.
|
|
23
|
+
Unknown scalar attributes are also checked. Regression examples prove the
|
|
24
|
+
helper rejects absent accessors, dropped nested fields, incorrect values and
|
|
25
|
+
raw hashes in place of typed nested models.
|
|
26
|
+
|
|
27
|
+
There are two explicit conversions rather than consulting the model's field
|
|
28
|
+
definitions to manufacture expected values: documented Date accessors use
|
|
29
|
+
`Date.parse`, and a null parcel array becomes `[]` under the existing array
|
|
30
|
+
casting contract. CSV cases independently list every expected column and its
|
|
31
|
+
typed value. Scalar client IDs and barcode lists are compared directly; empty
|
|
32
|
+
success objects have no leaf properties. PDF methods compare unchanged bytes,
|
|
33
|
+
including binary data. Error objects are checked through both a resource and a
|
|
34
|
+
collection response.
|
|
35
|
+
|
|
36
|
+
## Schema limitations and discrepancies
|
|
37
|
+
|
|
38
|
+
- Extended print's schema describes `data` as an integer array, while the
|
|
39
|
+
documentation describes a base64 string. Its example follows the documented
|
|
40
|
+
JSON representation.
|
|
41
|
+
- External carrier parcel details are documented as a map, so populated maps
|
|
42
|
+
now return typed values instead of being treated as arrays. Empty arrays
|
|
43
|
+
seen in recordings remain readable for compatibility.
|
|
44
|
+
- Calculation additional services, courier-service allowances and office
|
|
45
|
+
maximum parcel dimensions now expose schema-backed nested model readers.
|
|
46
|
+
- The schema's unresolved `OfficeRoutingInformation` reference is covered by
|
|
47
|
+
the documentation's [RoutingInformation definition, section 3.34](https://api.speedy.bg/api/docs/#href-routing-information)
|
|
48
|
+
and [Tour definition, section 3.34.1](https://api.speedy.bg/api/docs/#href-routing-information-tour).
|
|
49
|
+
Office responses now expose typed routing and tour readers; all three office
|
|
50
|
+
JSON examples contain populated routing data checked recursively.
|
|
51
|
+
- `WorkDayType` and `DeliveryLimitViolationAutoAdjustment` have unresolved references in the
|
|
52
|
+
available schema snapshot. Their example values are null; no nested contract
|
|
53
|
+
is invented for these structures.
|
|
54
|
+
- These examples test parsing and accessor fidelity. Existing VCR/live specs
|
|
55
|
+
remain the evidence for actual server behavior and account restrictions.
|
data/docs/VCR.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# VCR contract testing
|
|
2
|
+
|
|
3
|
+
The opt-in contract suite records real read-only API responses and replays them
|
|
4
|
+
later with external network access disabled. Each endpoint has an independent
|
|
5
|
+
cassette. Public nomenclature contracts run with stable Bulgarian defaults;
|
|
6
|
+
account and shipment reads require the fixture identifiers described in
|
|
7
|
+
`.env.example` and are structurally sanitized before being written.
|
|
8
|
+
|
|
9
|
+
CI runs the committed recordings separately from the opt-in live suite:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
SPEEDY_VCR_REPLAY=1 bundle exec rspec spec/recorded_contract_spec.rb
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Replay does not load `.env` files or require carrier credentials. It uses
|
|
16
|
+
`record: :none`, rejects unmatched requests, and blocks network access even if
|
|
17
|
+
a cassette's record mode is overridden. Missing cassettes cannot be recorded by CI.
|
|
18
|
+
|
|
19
|
+
PDF printing has read-only contracts in `spec/live/printing_contract_spec.rb`.
|
|
20
|
+
They use existing demo parcel/shipment IDs and never pass `printerName`, so no
|
|
21
|
+
physical printing is requested. Raw PDF bodies and base64 PDF data are replaced
|
|
22
|
+
entirely before recording; metadata is structurally sanitized. Replays verify
|
|
23
|
+
the response envelope, document signature and metadata types, not PDF contents
|
|
24
|
+
or visual correctness. The retained PDF signature is only kept when the actual
|
|
25
|
+
document starts with `%PDF-`; unexpected opaque bodies are replaced with a
|
|
26
|
+
non-PDF marker so a failed request cannot become a passing PDF fixture.
|
|
27
|
+
|
|
28
|
+
To record these contracts, set `SPEEDY_CONTRACT_PARCEL_IDS` and
|
|
29
|
+
`SPEEDY_CONTRACT_SHIPMENT_IDS` to existing, printable demo fixtures, then run:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
SPEEDY_LIVE_TESTS=1 bundle exec rspec spec/live/printing_contract_spec.rb
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
ZPL output is not covered by these PDF-specific contracts.
|
|
36
|
+
|
|
37
|
+
Offline request-shape specs exercise every explicit public keyword at least
|
|
38
|
+
once. Hash payloads and `**options` pass through generic serialization, so their
|
|
39
|
+
documented representative fields and model casting are tested without trying
|
|
40
|
+
every possible field combination. Live contracts use the smallest meaningful
|
|
41
|
+
payload for each server behavior; they are not a parameter-permutation suite.
|
|
42
|
+
|
|
43
|
+
## Coverage policy
|
|
44
|
+
|
|
45
|
+
| Endpoint kind | Contract approach |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| Public location and service nomenclatures | Recorded directly, one cassette per endpoint |
|
|
48
|
+
| Client, payout, calculation, shipment, tracking, and label-info reads | Recorded with dedicated demo fixtures and structural response redaction |
|
|
49
|
+
| Bulk CSV nomenclatures | Opt-in because Speedy may require account permission; one cassette per dataset |
|
|
50
|
+
| PDF labels, extended PDF labels, and PDF vouchers | Read-only fixtures; opaque document contents replaced entirely before recording |
|
|
51
|
+
| ZPL output | Offline coverage only; no live ZPL contract yet |
|
|
52
|
+
| Shipment, pickup, contact, and courier mutations | Excluded from the read-only suite; use the lifecycle pattern below |
|
|
53
|
+
|
|
54
|
+
## Mutating endpoints
|
|
55
|
+
|
|
56
|
+
VCR only records HTTP; it does not roll back the carrier. Projects that test
|
|
57
|
+
creation or updates against a real sandbox usually record a complete scenario:
|
|
58
|
+
|
|
59
|
+
1. create a uniquely identifiable shipment/contact/pickup in a dedicated demo
|
|
60
|
+
account;
|
|
61
|
+
2. exercise the update, handover, or cancellation;
|
|
62
|
+
3. query and assert the resulting server state;
|
|
63
|
+
4. cancel or delete the fixture in an `ensure` cleanup block; and
|
|
64
|
+
5. record and verify the cleanup response in the same cassette.
|
|
65
|
+
|
|
66
|
+
Recording is manual. CI only replays reviewed cassettes and must never have
|
|
67
|
+
network access or carrier credentials. If cleanup is unavailable or unreliable,
|
|
68
|
+
use a provider-reset sandbox, disposable pre-provisioned fixtures, or an offline
|
|
69
|
+
request-shape test instead of a live mutation.
|
|
70
|
+
|
|
71
|
+
Before committing a recording, verify cleanup in the carrier UI, inspect the
|
|
72
|
+
diff, scan for credentials and personal data, and retain JSON types when
|
|
73
|
+
replacing identifiers. Never bulk re-record the entire suite automatically;
|
|
74
|
+
delete and refresh one cassette at a time.
|
|
75
|
+
|
|
76
|
+
## Reviewed mutation example
|
|
77
|
+
|
|
78
|
+
`spec/live/shipment_mutation_contract_spec.rb` implements the lifecycle above
|
|
79
|
+
for shipment creation. It creates a synthetic office-delivery shipment and
|
|
80
|
+
cancels it in an `ensure` block, then calls `shipment_info` to document that
|
|
81
|
+
Speedy retains the cancelled shipment as a historical record. It then tracks
|
|
82
|
+
the created parcel with its full operation history. The demo recording returns
|
|
83
|
+
an empty operation list immediately after cancellation, which the example
|
|
84
|
+
asserts. All four interactions are stored in
|
|
85
|
+
`speedy_api/shipment_create_cancel_and_verify.yml`; the generated shipment ID
|
|
86
|
+
is filtered.
|
|
87
|
+
|
|
88
|
+
The example requires an additional, deliberate opt-in:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
SPEEDY_LIVE_TESTS=1 SPEEDY_MUTATING_LIVE_TESTS=1 \
|
|
92
|
+
bundle exec rspec spec/live/shipment_mutation_contract_spec.rb
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Before recording, verify that `SPEEDY_CONTRACT_OFFICE_ID` accepts service 505
|
|
96
|
+
and that the configured sender client may create shipments. A successful
|
|
97
|
+
`shipment/cancel` response is the authoritative cleanup confirmation. The
|
|
98
|
+
documented `shipment/info` response has no cancellation-status field, so the
|
|
99
|
+
final lookup verifies record retention and the generated identifier but cannot
|
|
100
|
+
independently prove cancellation. Tracking operation `128` means Canceled, but
|
|
101
|
+
the recorded uncollected shipment has no tracking operations after cancellation.
|
|
102
|
+
Neither an empty tracking history nor a retained shipment record independently
|
|
103
|
+
proves cancellation. This lifecycle relies on the documented successful cancel
|
|
104
|
+
response for cleanup confirmation. There is no documented dedicated cancellation
|
|
105
|
+
status field in the shipment information response.
|
|
106
|
+
|
|
107
|
+
## Mutation cleanup inventory
|
|
108
|
+
|
|
109
|
+
The classifications below describe the published API, not a guarantee that a
|
|
110
|
+
demo account accepts every payload. Cancellation is compensating cleanup: it
|
|
111
|
+
leaves a historical record and does not roll back all stored data.
|
|
112
|
+
Sources: [shipment, pickup, and contact API documentation](https://api.speedy.bg/api/docs/).
|
|
113
|
+
|
|
114
|
+
### Clear documented cleanup paths
|
|
115
|
+
|
|
116
|
+
| Endpoint | Scenario and cleanup | Recording status |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| Create and cancel shipment | Create an uncollected shipment without requesting pickup; cancel it in `ensure` | Existing demo cassette confirms successful cancellation |
|
|
119
|
+
| Update shipment | Create a disposable shipment, replace its contents, query the changed contents, cancel | Recorded as `shipment_update_and_cancel` |
|
|
120
|
+
| Update shipment properties | Create a disposable shipment, change the `contents` property, query it, cancel | Recorded as `shipment_properties_and_cancel` |
|
|
121
|
+
|
|
122
|
+
All scenarios are in `spec/live/shipment_mutation_contract_spec.rb` and use
|
|
123
|
+
separate cassettes. Cleanup runs even if an operation or assertion fails after
|
|
124
|
+
creation returns an identifier. If creation times out before returning an ID,
|
|
125
|
+
automatic cleanup cannot identify the remote fixture; reconcile the account
|
|
126
|
+
before retrying. Cleanup failures fail the example and require manual review.
|
|
127
|
+
No pickup or handover is performed.
|
|
128
|
+
|
|
129
|
+
Existing cassettes replay as usual; do not delete them without planning another
|
|
130
|
+
disposable fixture. Read-back assertions check contents before cleanup. Successful
|
|
131
|
+
`shipment/cancel` remains the cancellation confirmation, not tracking history.
|
|
132
|
+
|
|
133
|
+
### Unavailable in the standard demo account
|
|
134
|
+
|
|
135
|
+
Adding parcels and finalizing a pending shipment require permission to create
|
|
136
|
+
with `pendingParcels`. The standard test account does not have this permission,
|
|
137
|
+
so these endpoints have offline request/model coverage only. The live lifecycle
|
|
138
|
+
and its capability flag were removed. Cancelling the parent shipment is the
|
|
139
|
+
documented cleanup approach for an authorized account, but that path has not
|
|
140
|
+
been verified in the demo environment.
|
|
141
|
+
|
|
142
|
+
### Suggested cleanup with uncertain effectiveness
|
|
143
|
+
|
|
144
|
+
| Endpoint | Possible approach | Uncertainty |
|
|
145
|
+
| --- | --- | --- |
|
|
146
|
+
| Request pickup | Cancel every associated disposable shipment, then ask the carrier to cancel/reset the pickup order | Cancelling shipments is not documented to cancel the courier visit or pickup order; no pickup cancellation endpoint is exposed |
|
|
147
|
+
|
|
148
|
+
This is a suggestion only and has no live mutation spec. Confirm order-level
|
|
149
|
+
cleanup with the carrier before recording it.
|
|
150
|
+
|
|
151
|
+
### No obvious API cleanup path
|
|
152
|
+
|
|
153
|
+
| Endpoint/action | Missing inverse |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| Create contact | No contact-delete endpoint; overwriting a contact would not remove it and could alter an existing account record |
|
|
156
|
+
| Handover to courier | No undo-handover endpoint; do not assume shipment cancellation reverses a registered handover |
|
|
157
|
+
| Handover to midway carrier | No undo for the registered external handover |
|
|
158
|
+
| Direct printing using `printerName` | Physical printing cannot be undone through the API; read-only print contracts omit this option |
|
|
159
|
+
|
|
160
|
+
These need carrier-managed reset or explicitly disposable fixtures. They remain
|
|
161
|
+
covered offline rather than by automatic live mutation scenarios.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Speedy schema fidelity notes
|
|
2
|
+
|
|
3
|
+
The implementation is based on the JSON schemas distributed with Speedy's API
|
|
4
|
+
documentation. Those files describe payload shapes, but a schema file alone is
|
|
5
|
+
not evidence that a public HTTP route exists or remains enabled for every
|
|
6
|
+
account.
|
|
7
|
+
|
|
8
|
+
## Address search schemas without a route
|
|
9
|
+
|
|
10
|
+
The schema bundle contains `SearchAddressRequest`, `SearchAddressResponse`, and
|
|
11
|
+
`SearchAddress`. The request describes site, complex, street,
|
|
12
|
+
point-of-interest, and building-number inputs, while the response contains
|
|
13
|
+
address suggestions. However, the provided endpoint documentation does not
|
|
14
|
+
associate those types with an HTTP path.
|
|
15
|
+
|
|
16
|
+
No route is inferred from the Java package identifier or type name because an
|
|
17
|
+
incorrect route could create an apparently supported API that fails against
|
|
18
|
+
real accounts. Consequently, `Speedy::Client` does not expose address search.
|
|
19
|
+
It can be added compatibly when Speedy publishes an authoritative route and
|
|
20
|
+
request contract.
|
|
21
|
+
|
|
22
|
+
## Representation decisions
|
|
23
|
+
|
|
24
|
+
- JSON object responses with documented fields use typed `Speedy::Model`
|
|
25
|
+
subclasses.
|
|
26
|
+
- Batch responses use `Speedy::Collection`, preserving top-level API errors.
|
|
27
|
+
- PDF and ZPL endpoints return binary strings; the gem does not decode or write
|
|
28
|
+
them to disk.
|
|
29
|
+
- CSV nomenclature exports are parsed into the same model classes as JSON
|
|
30
|
+
nomenclature results.
|
|
31
|
+
- Fields whose nested shape is not defined authoritatively remain arrays,
|
|
32
|
+
hashes, or untyped model attributes rather than speculative classes.
|
|
33
|
+
- Undocumented fields are preserved in `Model#to_h`, so additive carrier
|
|
34
|
+
changes do not discard information.
|
|
35
|
+
|
|
36
|
+
These notes describe documentation-derived support. They are not a substitute
|
|
37
|
+
for testing with the Speedy environment and permissions assigned to a specific
|
|
38
|
+
contract.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
|
|
5
|
+
module Speedy
|
|
6
|
+
class CamelCaseMiddleware < Faraday::Middleware
|
|
7
|
+
DEFAULT_ACRONYMS = {
|
|
8
|
+
'id' => 'Id',
|
|
9
|
+
'ids' => 'Ids',
|
|
10
|
+
'cod' => 'COD',
|
|
11
|
+
'pudo' => 'PUDO'
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(app, acronyms: {}, replace_defaults: false)
|
|
15
|
+
super(app)
|
|
16
|
+
@acronyms =
|
|
17
|
+
if acronyms.nil?
|
|
18
|
+
{}
|
|
19
|
+
elsif replace_defaults
|
|
20
|
+
acronyms.transform_keys(&:to_s)
|
|
21
|
+
else
|
|
22
|
+
DEFAULT_ACRONYMS.merge(acronyms.transform_keys(&:to_s))
|
|
23
|
+
end
|
|
24
|
+
@acronym_reverse = @acronyms.each_with_object({}) { |(key, value), hash| hash[value] = key }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(env)
|
|
28
|
+
env.body = deep_transform_keys(env.body) { |key| to_camel_case(key) } if env.body.is_a?(Hash)
|
|
29
|
+
@app.call(env)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def on_complete(env)
|
|
33
|
+
env.body = deep_transform_keys(env.body) { |key| to_snake_case(key) } if env.body.is_a?(Hash)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def to_camel_case(key)
|
|
39
|
+
parts = key.to_s.split('_')
|
|
40
|
+
first = parts.shift
|
|
41
|
+
|
|
42
|
+
first + parts.map { |part| @acronyms.fetch(part, part.capitalize) }.join
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_snake_case(key)
|
|
46
|
+
str = key.to_s
|
|
47
|
+
@acronym_reverse.keys.sort_by { |acro| -acro.length }.each do |acro|
|
|
48
|
+
str = str.gsub(/#{acro}(?=[A-Z]|$)/) { "_#{@acronym_reverse[acro]}_" }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
str
|
|
52
|
+
.gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
|
53
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
54
|
+
.downcase
|
|
55
|
+
.gsub(/__+/, '_')
|
|
56
|
+
.sub(/^_/, '')
|
|
57
|
+
.sub(/_$/, '')
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def deep_transform_keys(obj, &block)
|
|
61
|
+
case obj
|
|
62
|
+
when Array
|
|
63
|
+
obj.map { |item| deep_transform_keys(item, &block) }
|
|
64
|
+
when Hash
|
|
65
|
+
obj.each_with_object({}) { |(key, value), hash| hash[yield(key)] = deep_transform_keys(value, &block) }
|
|
66
|
+
else
|
|
67
|
+
obj
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'logger'
|
|
6
|
+
|
|
7
|
+
module Speedy
|
|
8
|
+
class Client
|
|
9
|
+
include Speedy::ClientResources
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
attr_reader :instance
|
|
13
|
+
|
|
14
|
+
def configure
|
|
15
|
+
@instance ||= new
|
|
16
|
+
yield(@instance) if block_given?
|
|
17
|
+
@instance.setup_connection!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def connection
|
|
21
|
+
instance&.connection || raise('Speedy::Client is not configured. Call Speedy::Client.configure first.')
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attr_accessor :username, :password, :base_url, :adapter,
|
|
26
|
+
:language, :client_system_id,
|
|
27
|
+
:acronyms, :replace_acronym_defaults,
|
|
28
|
+
:debug, :debug_bodies, :debug_logger,
|
|
29
|
+
:open_timeout, :read_timeout, :write_timeout
|
|
30
|
+
|
|
31
|
+
def initialize
|
|
32
|
+
@base_url = 'https://api.speedy.bg/v1/'
|
|
33
|
+
@adapter = Faraday.default_adapter
|
|
34
|
+
@language = nil
|
|
35
|
+
@client_system_id = nil
|
|
36
|
+
@acronyms = {}
|
|
37
|
+
@replace_acronym_defaults = false
|
|
38
|
+
@debug = false
|
|
39
|
+
@debug_bodies = false
|
|
40
|
+
@debug_logger = Logger.new($stdout)
|
|
41
|
+
@open_timeout = 30
|
|
42
|
+
@read_timeout = 30
|
|
43
|
+
@write_timeout = 30
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def add_acronyms(hash)
|
|
47
|
+
@acronyms = (@acronyms || {}).merge(hash.transform_keys(&:to_s))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def setup_connection!
|
|
51
|
+
validate_credentials!
|
|
52
|
+
|
|
53
|
+
@connection = Faraday.new(base_url) do |conn|
|
|
54
|
+
configure_timeouts(conn)
|
|
55
|
+
conn.use Speedy::CamelCaseMiddleware,
|
|
56
|
+
acronyms: @acronyms,
|
|
57
|
+
replace_defaults: @replace_acronym_defaults
|
|
58
|
+
conn.request :json
|
|
59
|
+
conn.response :json, content_type: 'application/json'
|
|
60
|
+
configure_debug_logging(conn) if debug
|
|
61
|
+
conn.adapter(*Array(adapter))
|
|
62
|
+
conn.headers['User-Agent'] = "SpeedyRubyClient/#{Speedy::VERSION}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def connection
|
|
67
|
+
@connection || raise('Connection is not initialized. Call Speedy::Client.configure first.')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def credentials
|
|
71
|
+
{
|
|
72
|
+
user_name: username,
|
|
73
|
+
password: password,
|
|
74
|
+
language: language,
|
|
75
|
+
client_system_id: client_system_id
|
|
76
|
+
}.compact
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def configure_debug_logging(connection)
|
|
82
|
+
connection.response :logger, debug_logger, bodies: debug_bodies do |formatter|
|
|
83
|
+
[username, password, client_system_id].each do |value|
|
|
84
|
+
formatter.filter(value.to_s, '[FILTERED]') unless value.to_s.empty?
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def configure_timeouts(connection)
|
|
90
|
+
connection.options.open_timeout = open_timeout
|
|
91
|
+
connection.options.timeout = read_timeout
|
|
92
|
+
connection.options.write_timeout = write_timeout if connection.options.respond_to?(:write_timeout=)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def validate_credentials!
|
|
96
|
+
creds_missing = [username, password].any? { |credential| credential.to_s.empty? }
|
|
97
|
+
raise 'Username and password must be set' if creds_missing
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|