From 00826d605f622204bc26bae78b2dba3e74439ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=99=E6=B0=B4=E5=BF=8D?= <335298362+sensui624@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:03:36 +0800 Subject: [PATCH] docs: add CHANGELOG, rewrite README, require README/CHANGELOG updates in CLAUDE.md Docs only, no version bump. --- CHANGELOG.md | 171 +++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 11 +++ README.md | 203 +++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 371 insertions(+), 14 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2f0752c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,171 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- `CHANGELOG.md`. +- Full `README.md`: overview, requirements, installation, configuration, usage of every API, error handling, development and release flow. +- CLAUDE.md rule: every PR must also update `README.md` and `CHANGELOG.md`. + +## [2.1.0] - 2026-10-27 + +### Added + +- `history_id:` option for `Client#create_orders!` and `Client#create_sales!`: attach a batch to an existing import history on the XDelivery side, so several calls merge into one history. A blank (`nil`, `""`, whitespace) value is treated as absent and surrounding whitespace is stripped. +- `Response::Orders#history_id` (the history the batch was attached to; `nil` on a failed response). +- Support for Ruby 3.3 and 3.4: `required_ruby_version >= 2.7`, explicit `ostruct` dependency and require. +- GitHub Actions test matrix (Ruby 3.1.7, 3.3.12, 3.4.11) and `publish.yml` (RubyGems Trusted Publishing). +- CLAUDE.md release rules. + +### Changed + +- The `rest-client` dependency now requires `>= 2.0` (needed for `RestClient::Exceptions::Timeout`). +- `Rakefile` default task is now `t`. + +### Fixed + +- Request timeouts (`RestClient::Exceptions::Timeout`) used to be swallowed and surfaced as a confusing `NoMethodError` (`nil.code`). They are now raised as-is. + +## [2.0.3] - 2022-08-04 + +### Fixed + +- Release fixes after 2.0.0 (2.0.2 was never published to RubyGems). + +## [2.0.1] - 2022-08-04 + +### Fixed + +- Release fixes after 2.0.0 (version bump only). + +## [2.0.0] - 2022-08-04 + +### Added + +- `Xdelivery.env` / `Xdelivery.env=` and `Xdelivery.production?` / `Xdelivery.test?`. In any environment other than `production` requests go to the staging API (`https://api.staging.xdelivery.io`). +- `API::Base#base_url`, `PRODUCTION_BASE_URL` and `TEST_BASE_URL`. + +### Removed + +- `API::Base::BASE_URL` (replaced by the two constants above). + +## [1.0.2] - 2022-05-04 + +### Added + +- Error handling for HTTP error responses: `Client::NotFound` (404), `Client::InternalServerError` (500), `Client::BadGateway` (502), `Client::ServiceUnavailable` (503), `Client::GatewayTimeout` (504) and `Client::UnknownResponse` (non-JSON or unexpected status). + +## [1.0.1] - 2020-04-22 + +### Added + +- `Xdelivery.open_timeout`, `Xdelivery.read_timeout` and `Xdelivery.configure`. + +## [1.0.0] - 2019-10-21 + +### Added + +- Shops API: `Client#get_shops!`, and the `shop_id` order / sale column. + +### Removed + +- Invoice settings API (`Client#get_invoice_settings!`, `API::InvoiceSettings`) and the `invoice_setting_id` invoice column. + +## [0.2.4] - 2019-07-31 + +### Added + +- `Xdelivery::Callback` to verify XDelivery's callback `check_code`. + +## [0.2.3] - 2019-07-30 + +### Added + +- `merchant_note` (system note) order and sale column. + +## [0.2.2] - 2019-07-18 + +### Added + +- `company_title` invoice column. + +## [0.2.1] - 2019-07-17 + +### Added + +- `invoice_setting_id` in the sales invoice params. + +## [0.2.0] - 2019-07-16 + +### Added + +- Invoice settings API (`Client#get_invoice_settings!`) and the `invoice_setting_id` invoice column (both removed again in 1.0.0). + +## [0.1.6] - 2019-06-21 + +### Fixed + +- Fix in the sales API. + +## [0.1.5] - 2019-06-20 + +### Added + +- Sales API (`Client#create_sales!`). + +## [0.1.4] - 2019-06-13 + +### Added + +- Invoice items (`items` invoice column). + +## [0.1.3] - 2019-06-12 + +### Changed + +- Renamed the invoice type column. + +## [0.1.2] - 2019-06-12 + +### Added + +- Invoice support for orders. + +## [0.1.1] - 2018-12-21 + +### Added + +- Product update API (`Client#update_products!`). + +## [0.1.0] - 2018-11-16 + +### Added + +- Initial release. + +[Unreleased]: https://github.com/superlanding/xdelivery-ruby/compare/v2.1.0...HEAD +[2.1.0]: https://github.com/superlanding/xdelivery-ruby/releases/tag/v2.1.0 +[2.0.3]: https://github.com/superlanding/xdelivery-ruby/commit/b0b5776 +[2.0.1]: https://github.com/superlanding/xdelivery-ruby/commit/a82af2d +[2.0.0]: https://github.com/superlanding/xdelivery-ruby/commit/8d10d3a +[1.0.2]: https://github.com/superlanding/xdelivery-ruby/commit/e6a829e +[1.0.1]: https://github.com/superlanding/xdelivery-ruby/commit/53a9a6c +[1.0.0]: https://github.com/superlanding/xdelivery-ruby/commit/e4a25eb +[0.2.4]: https://github.com/superlanding/xdelivery-ruby/commit/c64600a +[0.2.3]: https://github.com/superlanding/xdelivery-ruby/commit/6a9a00e +[0.2.2]: https://github.com/superlanding/xdelivery-ruby/commit/410ee7a +[0.2.1]: https://github.com/superlanding/xdelivery-ruby/commit/82d7953 +[0.2.0]: https://github.com/superlanding/xdelivery-ruby/commit/2853d8c +[0.1.6]: https://github.com/superlanding/xdelivery-ruby/commit/d6e1c7b +[0.1.5]: https://github.com/superlanding/xdelivery-ruby/commit/463d97b +[0.1.4]: https://github.com/superlanding/xdelivery-ruby/commit/22bd18d +[0.1.3]: https://github.com/superlanding/xdelivery-ruby/commit/c5c5890 +[0.1.2]: https://github.com/superlanding/xdelivery-ruby/commit/5b36875 +[0.1.1]: https://github.com/superlanding/xdelivery-ruby/commit/42c5f6b +[0.1.0]: https://github.com/superlanding/xdelivery-ruby/commit/f4b2c20 diff --git a/CLAUDE.md b/CLAUDE.md index 1d9b7dd..ddabc00 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,3 +7,14 @@ - 跳版規則(Semantic Versioning):修 bug 跳 patch(x.y.Z);新增相容功能跳 minor(x.Y.0);不相容變更跳 major(X.0.0)。 - 版號只改在 PR 裡。AI 帳號只能開 branch 與 PR,不得合併到 master;只有真人帳號可以合併。合併就等於批准發佈。 - 不得為了讓測試通過而改變程式原本的行為。 + +## 文件與更新紀錄(README / CHANGELOG) + +- **每一個 PR 都必須同步更新文件**: + - 只要行為、用法、設定、相依套件或支援的 Ruby / Rails 版本有變,就要更新 `README.md`。 + - 一律要在 `CHANGELOG.md` 新增一筆條目([Keep a Changelog](https://keepachangelog.com/zh-TW/1.1.0/) 格式,分類用 Added / Changed / Deprecated / Removed / Fixed / Security)。 +- `CHANGELOG.md` 的寫法: + - 沒有跳版號的 PR:寫在最上方的 `## [Unreleased]` 底下。 + - 有跳版號的 PR:把條目寫在新的版本章節 `## [x.y.z] - YYYY-MM-DD`(日期為合併日),並同步更新檔尾的版本比較連結。 + - 版本號必須與 rubygems.org 上實際發佈的版本一致,不得寫還沒發佈的版本。 +- 只改文件、CI 或 CLAUDE.md 的 PR 不需要跳版號,但仍要在 `CHANGELOG.md` 的 `Unreleased` 補一筆。 diff --git a/README.md b/README.md index 8b08ce1..f8879fe 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,213 @@ -# Xdelivery::Ruby +# Xdelivery -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/xdelivery/ruby`. To experiment with that code, run `bin/console` for an interactive prompt. +[![Gem Version](https://badge.fury.io/rb/xdelivery.svg)](https://rubygems.org/gems/xdelivery) +[![Tests](https://github.com/superlanding/xdelivery-ruby/actions/workflows/test.yml/badge.svg)](https://github.com/superlanding/xdelivery-ruby/actions/workflows/test.yml) -TODO: Delete this and the text above, and describe your gem +Ruby client for the **XDelivery (火箭快遞)** merchant API: push orders and sales in batches, optionally with an invoice, update product stock figures, list shops, ping the API and verify the callbacks XDelivery sends back to you. -## Installation +> The gem is published on RubyGems as **`xdelivery`** (the repository and gemspec file are named `xdelivery-ruby`); the Ruby namespace is `Xdelivery`. + +## Requirements -Add this line to your application's Gemfile: +- Ruby `>= 2.7` (gemspec). CI runs on Ruby **3.1, 3.3 and 3.4** (see [`.github/workflows/test.yml`](.github/workflows/test.yml)). +- No Rails dependency. Runtime dependencies: `rest-client >= 2.0` and `ostruct`. +- A merchant number (`merchant_no`) and access key (`access_key`) issued by XDelivery. + +## Installation ```ruby -gem 'xdelivery-ruby' +# Gemfile +gem "xdelivery" ``` -And then execute: +```bash +bundle install +``` + +Or `gem install xdelivery`, then `require "xdelivery"`. - $ bundle +## Configuration -Or install it yourself as: +All settings are optional; the defaults are shown. - $ gem install xdelivery-ruby +```ruby +# config/initializers/xdelivery.rb +Xdelivery.configure do |config| + config.open_timeout = 5 # seconds to open the connection + config.read_timeout = 5 # seconds to wait for the response +end + +Xdelivery.env = :production # anything other than :production talks to the staging API +``` + +| Environment | Base URL | +|-------------|----------| +| `:production` (default) | `https://api.xdelivery.io` | +| any other value, e.g. `:test` | `https://api.staging.xdelivery.io` | ## Usage -TODO: Write usage instructions here +Create a client with your credentials. Every API method is a bang method on the client and returns a response object. + +```ruby +client = Xdelivery::Client.new("YOUR_MERCHANT_NO", "YOUR_ACCESS_KEY") +``` + +### Create orders + +`create_orders!` yields a builder; call `add(order_params, invoice_params = {})` for each order, and the batch is sent when the block returns. + +```ruby +response = client.create_orders! do |orders| + orders.add( + { + order_id: "SP19049", + provider: "FAMI", + recipient: "Eddie", + mobile: "0976077777", + email: "", + store_id: "F008263", + store_name: "全家鳳山埤頂店", + address: "高雄市鳳山區中山東路545號", + items: "白水*1, 黑水*1", + warehouse_items: [{ code: "白水", qty: 1 }, { code: "黑水", qty: 1 }], + total_order_amount: 1560, + cash_on_delivery: true, + order_created_at: "2018-04-12 15:23:31", + note: "Helloworld", + callback_url: "https://example.com/xdelivery/callback", + ref_id: 22345 + }, + # optional invoice + { create_type: "device", device: "email", email: "buyer@example.com", items: [] } + ) +end + +response.status? # => true when the API accepted the request +response.auth? # => false when the credentials were rejected (HTTP 401) +response.history_id # => import history id on XDelivery's side (nil on failure) +response.orders.each do |order| + order.order_id + order.valid? # false when order.errors is not empty + order.errors # => [] +end +``` + +Allowed order keys: `order_id provider recipient mobile email store_id store_name address items warehouse_items total_order_amount cash_on_delivery order_created_at note callback_url ref_id tag merchant_note shop_id`. +Allowed invoice keys: `create_type email company_code company_title donate_code device_id device items`. +**Any other key is silently dropped** by `add`. + +#### Attach to an existing import history (`history_id`) + +Pass `history_id:` to put several calls into the same import history on XDelivery's backend. A blank value (`nil`, `""`, whitespace) is ignored, and surrounding whitespace is stripped. + +```ruby +first = client.create_orders! { |orders| orders.add(order_a) } +second = client.create_orders!(history_id: first.history_id) { |orders| orders.add(order_b) } +``` + +### Create sales + +Same shape as orders, for sales batches; the builder is `Xdelivery::API::Sales`, `history_id:` is supported, and the response is also a `Response::Orders`. + +```ruby +response = client.create_sales! do |sales| + sales.add({ order_id: "SP19050", recipient: "Eddie", tracking_code: "T123", total_order_amount: 100 }) +end +``` + +Allowed sale keys: `order_id recipient mobile email address items warehouse_items total_order_amount cash_on_delivery order_created_at note callback_url ref_id tag tracking_code merchant_note shop_id` (invoice keys as for orders). + +### Update products + +```ruby +response = client.update_products! do |products| + products.set("迷黑", days_produce: 10, qty_day_average: 10, qty_week_average: 10, qty_month_average: 10, qty_owe: 10) +end +response.status? +``` + +Allowed keys: `days_produce qty_day_average qty_week_average qty_month_average qty_owe` (others are dropped). + +### Shops and ping + +```ruby +shops = client.get_shops! +shops.status? +shops.shops.each { |shop| [shop.id, shop.title, shop.pay2go_merchant_id, shop.default_shop] } + +client.ping!.status? +``` + +### Verify a callback + +XDelivery calls the `callback_url` you gave on an order. `Xdelivery::Callback` checks the signature (`check_code`, a SHA-256 over the sorted parameters plus your merchant number and access key): + +```ruby +# e.g. in a Rails controller (params is the callback request's params) +callback = Xdelivery::Callback.new(merchant_no, access_key, params.to_unsafe_h.symbolize_keys) + +if callback.valid? + callback.action # e.g. "delivered" + callback.title # e.g. "消費者取貨" + callback.desc + callback.provider # e.g. "fami" + callback.created_at # Time (Time.zone.parse when ActiveSupport's Time.zone is available) + # outside Rails, `require "time"` yourself first (the gem only calls Time.parse) +end +``` + +### Error handling + +The response object raises when the HTTP status is not `200` or `401`: + +| HTTP status | Exception | +|-------------|-----------| +| 404 | `Xdelivery::Client::NotFound` | +| 500 | `Xdelivery::Client::InternalServerError` | +| 502 | `Xdelivery::Client::BadGateway` | +| 503 | `Xdelivery::Client::ServiceUnavailable` | +| 504 | `Xdelivery::Client::GatewayTimeout` | +| any other status, or a body that is not JSON | `Xdelivery::Client::UnknownResponse` | + +Network timeouts are **not** wrapped: `RestClient::Exceptions::OpenTimeout` / `ReadTimeout` are raised as-is, so rescue them yourself: + +```ruby +begin + client.create_orders! { |orders| orders.add(order) } +rescue Xdelivery::Client::ServiceUnavailable, RestClient::Exceptions::Timeout + # retry later +end +``` + +A `401` is not an exception: check `response.auth?`. ## Development -After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +```bash +git clone https://github.com/superlanding/xdelivery-ruby.git +cd xdelivery-ruby +bin/setup +bundle exec rake t # runs spec/**/*_spec.rb with Minitest + WebMock (the default rake task does too) +bin/console +``` + +The specs stub HTTP with WebMock; `spec/real_client_spec.rb` (calls against a real server) is commented out. + +## Releasing + +Releases are automated with RubyGems [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/) (no API key in the repo): + +1. In your PR, bump `Xdelivery::VERSION` in `lib/xdelivery/version.rb` ([Semantic Versioning](https://semver.org/)). +2. Add a matching `## [x.y.z] - YYYY-MM-DD` section to `CHANGELOG.md`. +3. A human reviews and merges the PR into `master` (AI accounts can only open PRs). +4. [`.github/workflows/publish.yml`](.github/workflows/publish.yml) runs on `master`: when the gemspec version is not on rubygems.org yet it runs `rake release` (tags `vX.Y.Z` and publishes). A merge without a version bump publishes nothing. -To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). +Docs/CI-only PRs do not need a version bump. See [CLAUDE.md](CLAUDE.md): every PR must also update `README.md` (when behavior or usage changes) and add an entry to `CHANGELOG.md`. ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/xdelivery-ruby. +Bug reports and pull requests are welcome on GitHub at https://github.com/superlanding/xdelivery-ruby. ## License