Skip to content

feat: add the Mandi provider plugin - #13

Open
ameersohel45 wants to merge 2 commits into
feat/41-oan-adapter-pluginsfrom
feat/8-mandi-plugin
Open

feat: add the Mandi provider plugin#13
ameersohel45 wants to merge 2 commits into
feat/41-oan-adapter-pluginsfrom
feat/8-mandi-plugin

Conversation

@ameersohel45

Copy link
Copy Markdown
Collaborator

Targets feat/41-oan-adapter-plugins rather than development, because it builds on the machinery that PR #2 introduces. Merge that one first.

What

A second provider capability, openagrinet:MandiPrice, served against Agmarknet's Vistaar select. Additive: six new files, one line added to the plugin build list, and nothing existing changed.

The domain package — pkg/plugin/implementation/mandi/

38 lines, plus a 20-line prerequisites file that is deliberately empty. It supplies its name and nothing else; recognising the capability, resolving the call plan, authenticating, calling with the registry's budget and translating in both directions are all internal/upstream's.

internal/upstream needed no change for it. That was the test of whether the machinery and domain split in PR #2 actually held, and it is the reason this PR is as small as it is.

No prerequisites, and not by luck

A MandiPrice select names the market it wants — market.marketCode, market.district, market.state — and carries a commodity code and a validity window, which is every parameter the upstream takes. The pack has no top-level location, so nothing has to turn a point into a market.

That matters because turning a point into a market is exactly what the provider backend needs a PostGIS query for, and the adapter may not talk to a database. The pack's shape and the upstream's contract happen to agree, so the plugin is data only.

The mapping — config/mappings/agmarknet/mandi-price.select.yaml

Where the actual work is. Three things in it are not obvious:

  • the upstream's records use Title Case keys containing spaces`Modal Price` — so they need backticks, and its prices are strings, so they need $number() before they satisfy the pack's numeric types. A verbatim capture from the provider backend's own documentation is pinned as a fixture, so neither can drift unnoticed.
  • dates convert twice. The pack speaks ISO, the upstream speaks dd-MM-yyyy, so the request half converts out and the response half converts back.
  • the pack requires none of the fields the upstream needs. An OnDemand select requires only supportedCommodities and supportedPriceFields, leaving market and validity optional, and defines market.district and market.state as "name or governed code". So a spec-valid select can still be unanswerable, and the required: block refuses those with its own message rather than earning a 400 from Agmarknet — or worse, an empty result that reads as "no prices".

Resource ids are built from codes rather than the names the upstream reports: Kasdol APMC and Paddy(Common) carry spaces and brackets, and an id a consumer may put in a URL should not.

Query-string auth

Agmarknet takes its token as a query parameter. authScheme: query for that arrives in PR #2, since it is machinery rather than anything mandi-specific. The config here holds the parameter's name and the name of the environment variable carrying the value, never the value.

Why

Two capabilities from two domain packages in one pipeline, dispatched by binding key read from the payload. Which one answers is decided by the request, not by a routing table, a URL or a domain — each step recognises its own key and passes through anything else.

It also settles a claim PR #2 could only assert: that adding a domain costs a small package and a mapping. It cost 58 lines of Go and one line in the build script.

Testing

go vet ./... clean and go test ./... green across 63 packages.

The shipped mapping is run through the real mapper and the real provider step against a captured Agmarknet response, rather than asserted about. Both directions validate with jsonschema — the select against beckn.yaml and MandiPrice v0.1 in OnDemand mode, the on_select against beckn.yaml and the pack in Direct mode — with zero errors.

End to end on the local stack, both capabilities loaded into one provider adapter:

  • a select naming the mandi provider returns one Direct resource per price record, prices converted from strings, dates in ISO, the offer's references rewritten to what was returned, and a record that reported no minimum or maximum coming back with those absent rather than zeroed
  • a select naming the weather provider still returns its per-day forecast, unaffected
  • a publish and a discover for each capability, since publishing is capability-agnostic and a catalogue entry advertises OnDemand while carrying no prices

The local Postman collection covers all of that: 8 requests, 39 assertions, no failures.

Notes for review

  • Tested against a mock that reproduces the awkward parts of the real API deliberately — Title Case keys with spaces, string prices, dd-MM-yyyy, and a query token it rejects when absent. The real API's credentials, host and exact record set are untested.
  • v1 only. Agmarknet's v2 endpoint takes a point rather than market codes and resolves the market itself, which fits our payload better — but it needs a POST token exchange, and that needs pre-call I/O whose design is still open. Not attempted here.
  • One behaviour the provider backend has and this does not: when a market-scoped query returns nothing it retries without marketcode, widening to the district. Our step makes one call, so an empty result stays empty. Left alone until the real API's behaviour is observed, along with the question of whether Agmarknet reports "no data" as an HTTP 400.

Closes #8

Serves openagrinet:MandiPrice against Agmarknet's Vistaar select, as a sibling
to weather: a domain package of 58 lines wrapping internal/upstream, which
needed no change for it. That was the test of whether the machinery and domain
split from 2b3cab1 actually held, and it did.

The package has NO prerequisites, and the reason is the pack rather than luck.
A MandiPrice select names the market it wants -- market.marketCode,
market.district, market.state -- and carries a commodity code and a validity
window, which is every parameter the upstream takes. There is no top-level
location in the pack, so nothing has to turn a point into a market, which is
the one thing the provider backend needs a spatial SQL query for and the one
thing this adapter may not do.

The mapping carries the whole contract. Three things in it are not obvious:

  the upstream's records use Title Case keys WITH SPACES -- `Modal Price` --
  so they need backticks, and its prices are STRINGS, so they need $number
  before they satisfy the pack's numeric types. Both are pinned by a verbatim
  capture from the provider backend's own documentation.

  dates convert twice. The pack speaks ISO, the upstream speaks dd-MM-yyyy, so
  the request half converts out and the response half converts back.

  the pack requires none of the fields the upstream needs -- an OnDemand
  select requires only supportedCommodities and supportedPriceFields, leaving
  market and validity optional. So a spec-valid select can be unanswerable,
  and the mapping's required: block refuses those with its own message rather
  than earning a 400 or, worse, an empty result that reads as "no prices".

Resource ids are built from codes rather than the names the upstream reports:
"Kasdol APMC" and "Paddy(Common)" carry spaces and brackets, and an id a
consumer may put in a URL should not.

Verified: the shipped mapping run through the real mapper and the real step
answers two records as two Direct resources with their prices converted, the
offer's references rewritten to match, and absent min/max left absent rather
than zeroed. Both directions validate -- the select against beckn.yaml and
MandiPrice v0.1 in OnDemand mode, the on_select against beckn.yaml and the
pack in Direct mode, with no errors.
Adds mandi alongside weather in config/oan-provider-adapter.yaml, so the
reference shows the thing that is actually interesting about this design: two
domain packages in one pipeline, sharing the module, the registry client and
the mapper, and sharing nothing else.

The whole cost of the second capability is one providerSteps entry and one
line in steps. No routing table, no new module, no new port. Which one answers
is decided by the payload -- each step builds a binding key from it, serves the
request if the key is its own, and passes it through untouched if not -- so the
order they appear in does not matter either.

mandi uses authScheme query, because Agmarknet's Vistaar API takes its token as
a query parameter. The adapter holds the parameter's name and the name of the
environment variable carrying the value, never the value, and redacts it from
the URL it logs -- so a token cannot reach the log by way of the request.

Verified by booting this config in an image that has both plugins: both
ProviderStep plugins load, the pipeline initialises as
[validateSign validateSchema weather mandi signAck], and the module registers
at /. Worth noting the published adapter image does NOT yet carry mandi.so, so
against that image this config fails at startup with "plugin mandi not found"
until it is rebuilt from this branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant