digiwin_dsp

CI codecov Ruby License: MIT

Ruby client for the Digiwin DSP 自有官網模組 (Official Website Module) API. Lets a Rails 自有官網 push orders, cancellations, invoice updates, and returns into the Digiwin ERP through the DSP gateway.

Operation Resource Endpoint
新增訂單 DigiwinDsp::Resources::Order POST /v1/SalesOrder/add
取消訂單 DigiwinDsp::Resources::Cancellation POST /v1/SalesOrder/cancel
發票更新 DigiwinDsp::Resources::Invoice POST /v1/SalesOrder/invoice
退貨 DigiwinDsp::Resources::Return POST /v1/SalesOrder/return

See docs/dsp-api-spec.md (plus docs/dsp-specs/*.yaml) for the wire spec.

Installation

# Gemfile
gem "digiwin_dsp"
bundle install

Ruby ≥ 3.2 required.

Configuration

Configure once at boot (e.g. config/initializers/digiwin_dsp.rb in Rails):

DigiwinDsp.configure do |c|
  c.api_key      = ENV.fetch("DIGIWIN_DSP_API_KEY")
  c.platform_id  = ENV.fetch("DIGIWIN_DSP_PLATFORM_ID")
  c.environment  = :sandbox          # :sandbox (UAT) | :production
  c.logger       = Rails.logger      # any Logger-like object
  c.timeout      = 10                # request timeout (seconds)
  c.open_timeout = 5
end

Every setting also falls back to an ENV var:

Setting ENV var Default
api_key DIGIWIN_DSP_API_KEY (required)
api_secret DIGIWIN_DSP_API_SECRET nil
platform_id DIGIWIN_DSP_PLATFORM_ID nil
environment DIGIWIN_DSP_ENV :sandbox
base_url DIGIWIN_DSP_BASE_URL resolved from environment
timeout 10
open_timeout 5
logger Logger.new(IO::NULL)

Base URLs (resolved from environment):

  • :sandboxhttps://digiwindsp.digiwin.com/DSP_UAT/api/DSP
  • :productionhttps://digiwindsp.digiwin.com/DSP/api/DSP

See .env.local.example for a starter env file.

Usage

Each resource exposes a single #create(records, idempotency_key:, digi_header:) method that returns the parsed response_detail array on success or raises a typed exception on failure.

Create an order

record = {
  "platform_id"     => "acme_storefront_test",
  "create_datetime" => Time.now.strftime("%Y-%m-%d %H:%M:%S"),
  "site_no"         => "acme_storefront_test",
  "form_no"         => "WEB202605200001",     # 官網訂單編號
  "order_date"      => "20260520",
  "buyer_name"      => "王小明",
  "receiver_name"   => "王小明",
  "pay_type"        => "9104",
  "shipping_type"   => "9102",
  "tax_type"        => "1",
  "sno"             => "1",                   # line index
  "form_subno"      => "1",
  "product_no"      => "P-001",
  "product_name"    => "測試商品",
  "unit"            => "EA",
  "qty"             => "1",
  "free_qty"        => "0",
  "price"           => "100",
  "subtotal"        => "100",
  "payment"         => "100",
  "order_status"    => "3",                   # 3 = 新增
  "last_record"     => "Y"                    # "Y" on the final line
}

response_detail = DigiwinDsp::Resources::Order.create(record)
# => [{ "form_no" => "WEB202605200001", ... }]

Multi-line orders: pass an array. Each element must carry the order-level fields plus its own line fields. Set "last_record" => "Y" on the final element and "N" on the rest:

records = [
  base_fields.merge("sno" => "1", "product_no" => "P-001", "qty" => "1", "last_record" => "N"),
  base_fields.merge("sno" => "2", "product_no" => "P-002", "qty" => "3", "last_record" => "Y")
]

DigiwinDsp::Resources::Order.create(records)

Cancel, invoice update, return

DigiwinDsp::Resources::Cancellation.create(cancel_record)
DigiwinDsp::Resources::Invoice.create(invoice_record)
DigiwinDsp::Resources::Return.create(return_record)

Each has its own required-field set (8 / 11 / 19 fields respectively). Inspect REQUIRED_FIELDS for the exact list, e.g.:

DigiwinDsp::Serializers::CancellationSerializer::REQUIRED_FIELDS

Idempotency

Pass idempotency_key: to attach an X-Idempotency-Key request header. DSP also dedupes server-side by form_no + platform_id and returns Duplicated:訂單不可重複 on a re-send (mapped to DuplicateRequestError).

DigiwinDsp::Resources::Order.create(record, idempotency_key: "order-#{record['form_no']}")

Background jobs

The gem is synchronous on purpose. Wrap calls in your own job runner:

class SyncOrderToDigiwinJob < ApplicationJob
  retry_on DigiwinDsp::RateLimitError, wait: :polynomially_longer, attempts: 5
  discard_on DigiwinDsp::DuplicateRequestError

  def perform(order_id)
    order = Order.find(order_id)
    DigiwinDsp::Resources::Order.create(order.to_dsp_payload, idempotency_key: "order-#{order.id}")
  end
end

Error handling

All exceptions inherit from DigiwinDsp::Error and carry rich attributes (#code, #dsp_message, #http_status, #request_id, #response_body).

Exception Raised when
DigiwinDsp::ConfigurationError api_key missing at request time
DigiwinDsp::ValidationError Required field missing locally, or DSP returns WrongStatus: / Processing:取消訂單處理中 / HTTP 400
DigiwinDsp::AuthenticationError HTTP 401 / 403
DigiwinDsp::DuplicateRequestError DSP returns Duplicated: or HTTP 409
DigiwinDsp::RateLimitError DSP returns Processing:資料處理中 (retryable) or persistent HTTP 429
DigiwinDsp::ServerError DSP returns 系統異常: or HTTP 5xx that exhausts retries
DigiwinDsp::NetworkError TCP connect failure or timeout
DigiwinDsp::Error Catch-all (unmapped failure message or unexpected status)

⚠️ Digiwin DSP returns HTTP 200 even on application-level failure. The gem parses the response body's Status / Message fields and raises the appropriate typed exception so callers can rescue normally.

begin
  DigiwinDsp::Resources::Order.create(record)
rescue DigiwinDsp::DuplicateRequestError
  Rails.logger.info("Order already pushed, skipping")
rescue DigiwinDsp::ValidationError => e
  Rails.logger.error("Payload rejected: #{e.dsp_message}")
  raise
rescue DigiwinDsp::RateLimitError, DigiwinDsp::ServerError
  raise   # let the job retry
end

Custom digi_header

By default the gem omits digi_header from the request body (it's only required for certain custom Digiwin integrations). If your DSP setup expects one, pass it through:

DigiwinDsp::Resources::Order.create(
  record,
  digi_header: {
    "digi_host"    => { "prod" => "EC-SHOP", "ip" => "10.0.0.42", "timestamp" => "20260520123456789" },
    "digi_service" => { "prod" => "ECP", "name" => "salesorder.add" }
  }
)

Development

bin/setup              # bundle install
bundle exec rspec      # run the full test suite (134 examples, 100% coverage)
bundle exec rubocop    # lint
bin/console            # IRB with the gem loaded

The full DSP OpenAPI 3.1 specs live under docs/dsp-specs/. If Digiwin updates them, replace the YAML files and re-run the test suite — the REQUIRED_FIELDS constants in each serializer pin the contract.

Contributing

  1. Fork & branch
  2. bin/setup
  3. Write tests first (TDD); ensure bundle exec rspec and bundle exec rubocop are green
  4. Open a PR

License

MIT. See LICENSE.txt.