> ## Documentation Index
> Fetch the complete documentation index at: https://docs.numeral.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Solidus

> Real-time tax calculation at checkout for Solidus stores via the solidus_numeral extension.

The `solidus_numeral` gem plugs into Solidus's pluggable tax calculator API to calculate tax in real time at checkout through Numeral's tax engine. Completed orders are automatically committed to Numeral's transaction ledger for filing — the tax charged to the customer is exactly what gets filed. RMA reimbursements, order cancellations, and short-ships are reported to Numeral as refunds automatically, and customer exemptions are managed per-user from the Solidus admin.

### System requirements

| Requirement | Version      |
| ----------- | ------------ |
| Solidus     | >= 4.6, \< 5 |
| Rails       | >= 7.2       |
| Ruby        | >= 3.2       |

### Prerequisites

Before installing, make sure you have:

1. A **Numeral account** at [dashboard.numeralhq.com](https://dashboard.numeralhq.com)
2. **Deploy access** to your Solidus application (Gemfile, initializers, migrations)
3. A **production-grade ActiveJob backend** (Sidekiq, GoodJob, SolidQueue, ...) — transaction commits, refunds, and syncs run through ActiveJob, and with the default inline/async adapters a Numeral outage could impact request handling and failed jobs won't be retried
4. **Outbound HTTPS** from your Rails servers to `api.numeralhq.com`

***

## Part 1: Connect Solidus in the Numeral dashboard

1. In the Numeral dashboard, go to **Connections**.
2. Click **Add Connection** and select **Solidus**.
3. The connection provides your pair of secret API keys:

| Key           | Mode                                                                                                                  |
| ------------- | --------------------------------------------------------------------------------------------------------------------- |
| `sk_test_...` | Test mode — calculations and transactions are recorded in test mode and never filed. Use this in development/staging. |
| `sk_live_...` | Live mode, for production.                                                                                            |

<Note>
  Which mode the gem runs in is determined entirely by which key you configure — there is no separate test-mode flag. Start with the `sk_test_` key and switch to the `sk_live_` key when you go to production.
</Note>

***

## Part 2: Install the gem

The `solidus_numeral` gem is provided by Numeral — reach out to your Numeral implementation contact for access if you don't already have it.

Add it to your Gemfile:

```ruby theme={null}
# Gemfile
gem "solidus_numeral"
```

Then install:

```bash theme={null}
bundle install
bin/rails generate solidus_numeral:install
```

The generator creates `config/initializers/solidus_numeral.rb`, copies the gem's migrations into your app, and offers to run them.

<Accordion title="Prefer to wire things up manually?">
  Create the initializer yourself:

  ```ruby theme={null}
  # config/initializers/solidus_numeral.rb
  SolidusNumeral.configure do |config|
    config.api_key = ENV["NUMERAL_API_KEY"]
  end

  Spree.config do |config|
    config.tax_calculator_class = "SolidusNumeral::TaxCalculator"
    config.shipping_rate_tax_calculator_class = "SolidusNumeral::ShippingRateTaxCalculator"
  end
  ```

  Then copy and run the migrations:

  ```bash theme={null}
  bin/rails railties:install:migrations FROM=solidus_numeral
  bin/rails db:migrate
  ```
</Accordion>

***

## Part 3: Configure the initializer

Set your API key from Part 1 (the `sk_test_` key first) in the environment variable the generated initializer reads:

```ruby theme={null}
# config/initializers/solidus_numeral.rb
SolidusNumeral.configure do |config|
  config.api_key = ENV["NUMERAL_API_KEY"]
end

Spree.config do |config|
  config.tax_calculator_class = "SolidusNumeral::TaxCalculator"
  config.shipping_rate_tax_calculator_class = "SolidusNumeral::ShippingRateTaxCalculator"
end
```

### Configuration reference

| Setting                 | Default                                  | Description                                                                                                                                                           |
| ----------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `api_key`               | —                                        | **Required.** Your Numeral secret key (`sk_test_...` or `sk_live_...`). Which key is configured determines test vs. live mode.                                        |
| `api_base`              | `https://api.numeralhq.com`              | Base URL of the Numeral API.                                                                                                                                          |
| `enabled`               | `true`                                   | Kill switch. When `false` the calculator returns zero tax and the ledger jobs no-op, without touching the API.                                                        |
| `cache_duration`        | `3.hours`                                | Calculation response cache TTL. Must stay below Numeral's 24-hour calculation expiry.                                                                                 |
| `tax_included_in_price` | `nil`                                    | When `nil`, tax-inclusive pricing is derived per order from `Spree::TaxRate#included_in_price`. Set `true`/`false` to override globally.                              |
| `logger`                | `nil`                                    | Defaults to `Rails.logger`.                                                                                                                                           |
| `exception_handler`     | logs the error                           | Called with any exception raised during checkout tax calculation. The calculator itself never raises — it degrades to zero tax so an API outage can't break checkout. |
| `discount_calculator`   | `SolidusNumeral::DiscountCalculator.new` | Order-level discount proration. Must respond to `call(order)` and return `{line_item_id => BigDecimal discount share}`.                                               |

***

## Part 4: Map tax categories to Numeral product categories

Numeral decides taxability per line from its product taxonomy. The gem sends each line item's `Spree::TaxCategory#tax_code` (a core Solidus column) as the Numeral `product_category`:

1. Find the valid codes in the [Product Categories](/essentials/product-categories) reference.
2. In the Solidus admin, set **Tax Code** on each of your tax categories to the matching Numeral code (e.g. `CLOTHING`, `FOOD_BEVERAGE_GROCERIES`, ...).

<Note>
  Lines with no mapped code fall back to `GENERAL_MERCHANDISE` — calculations never fail on an unmapped SKU.
</Note>

### Product sync (optional, recommended)

Calculations auto-create products from SKUs, so no pre-sync is required. To make your catalog visible in the Numeral dashboard (and eligible for AI categorization and ops review), push it once:

```bash theme={null}
bin/rails numeral:sync_products
```

The task iterates every variant with a SKU, is idempotent (re-runs skip existing products), and throttles itself under Numeral's rate limit. It runs `SolidusNumeral::SyncProductsJob` and writes a summary to the `solidus_numeral_sync_logs` table (`action=product_sync`).

***

## Part 5: Mark customers tax-exempt (optional)

Visit `/admin/users/:id/numeral_information` for any user to manage their Numeral tax settings. The screen is not injected into the admin user tabs — link it wherever fits your admin. From there you can:

* **Mark the user tax exempt.** The gem syncs a customer record to Numeral (`is_tax_exempt: true`) and subsequent calculations for that user reference it and return zero tax. Unchecking syncs the revocation.
* **Store a VAT identification number.** Calculations then send the user as a `BUSINESS` customer with the VAT id (typed per the order's destination country, e.g. `eu_vat` / `gb_vat`), enabling EU B2B reverse charge. If the destination country has no supported tax id type, the id is omitted and a warning is logged.

Guests and non-exempt users need no customer sync.

***

## Testing it out

With your `sk_test_` key configured, everything is recorded in Numeral's test mode and never filed:

1. Add a product to the cart on your staging storefront and proceed to checkout with a complete US shipping address.
2. At checkout, verify a tax adjustment appears (labeled e.g. **Sales Tax (8.875%)**).
3. Complete the order — the gem commits it to Numeral in the background.
4. In the Numeral dashboard (in test mode), confirm the calculation and the committed transaction appear.
5. Create and reimburse an RMA for the order, then confirm a partial refund is recorded against the transaction in Numeral.

<Note>
  Tax appears once you enable collection for a jurisdiction in Numeral — until then calculations return 0% "no nexus" results. That's by design, not an error.
</Note>

### Where to look when something's wrong

* **`solidus_numeral_sync_logs` table** — every ledger attempt (commit, refund, void, customer sync, product sync) is recorded with its action, status, Numeral id, and error message, so silent background-job failures are visible.
* **Rails logs** — checkout calculation failures are routed through the configured `exception_handler`, which by default logs `[solidus_numeral] tax calculation failed - ...`. Checkout is never blocked: any failure yields zero tax instead of raising.
* **Misconfiguration warning** — if the extension is disabled or has no API key, the gem logs `[solidus_numeral] tax calculation is OFF ... - all orders are getting zero tax` (once per process) so a misconfigured store doesn't silently under-collect.

***

## How it works

* **Calculation caching.** Solidus recalculates taxes on every cart mutation and checkout transition (5–15 times per checkout). Responses are cached in `Rails.cache` keyed on the serialized calculation request, so identical order states cost nothing and any change to lines, address, or discounts busts the cache naturally. Default TTL is 3 hours (Numeral calculations stay valid for 24).
* **Checkout never breaks.** Missing/incomplete addresses, empty carts, the kill switch, and API errors all yield zero tax instead of raising.
* **Order lifecycle.** Orders are committed to Numeral when finalized. Cancellation triggers a full refund (or the remaining headroom, if partial refunds exist), RMA reimbursement triggers a partial refund grouped per original line item, and short-shipping triggers a delta partial refund. Refund amounts are capped against the amounts actually committed to Numeral, and the gem reconciles its local refund ledger against Numeral's records before every refund, so job retries never double-report.
* **Shipping.** Each shipment is sent as a `SHIPPING` line item, so shipping taxability follows each state's rules. The delivery-step shipping *rate* tax display is intentionally a no-op (it would cost one API call per candidate rate for a display-only number); shipping tax is calculated and charged as soon as the order recalculates after rate selection.
* **Tax-inclusive (VAT) prices.** Whether amounts are treated as tax-inclusive is decided per order: only when an `included_in_price` `Spree::TaxRate`'s zone covers the order's tax address (so US-destination orders on a mixed US+VAT store stay exclusive). Override globally with `config.tax_included_in_price` if that heuristic doesn't fit your store.
* **Discounts.** Line-level promotions are already reflected in the taxable amounts sent to Numeral; order-level promotions are prorated across line items (by pre-discount line total) before encoding.
* **Placeholder tax rate.** Solidus requires each tax adjustment to reference a `Spree::TaxRate`; the gem maintains a single zero-amount rate named `"Numeral Tax"` for this. Don't delete it or attach zones/categories to it.

***

## Troubleshooting

<AccordionGroup>
  <Accordion title="Tax not appearing at checkout">
    1. Confirm `config.api_key` is set and `config.enabled` is `true` — if not, the log contains `[solidus_numeral] tax calculation is OFF` (logged once per process).
    2. Confirm the shipping address is complete — calculations are skipped for missing or incomplete addresses (US/CA addresses also require a state).
    3. Confirm you've enabled tax collection for the destination jurisdiction in Numeral — until then, calculations legitimately return 0% "no nexus" results.
    4. Check the Rails log for `[solidus_numeral] tax calculation failed` entries from the exception handler.
  </Accordion>

  <Accordion title="Orders complete but no transactions appear in Numeral">
    Commits run through ActiveJob. Confirm your job backend is running and processing the gem's jobs, then check the `solidus_numeral_sync_logs` table for `action=commit` rows and their error messages. With the test key configured, also confirm you're viewing the Numeral dashboard in **test mode**.
  </Accordion>

  <Accordion title="Tax shows in the wrong mode (test vs. live)">
    There is no test-mode flag — the mode is determined entirely by which key is configured. Verify whether `NUMERAL_API_KEY` holds an `sk_test_` or `sk_live_` key in that environment.
  </Accordion>

  <Accordion title="A tax rate named &#x22;Numeral Tax&#x22; appeared in my admin">
    That's the gem's placeholder `Spree::TaxRate` — Solidus requires every tax adjustment to reference a persisted rate, and the actual amounts come from Numeral. Don't delete it or attach zones/categories to it.
  </Accordion>

  <Accordion title="VAT id isn't applied to an order">
    The VAT id is typed per the order's destination country (e.g. `eu_vat` / `gb_vat`). If the destination country has no supported tax id type, the id is omitted from the calculation and a warning is logged.
  </Accordion>
</AccordionGroup>

***

## Need help?

Reach out to your Numeral implementation contact or email [support@numeralhq.com](mailto:support@numeralhq.com).
