Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cbbb8e7
feat: let a mapping refuse a provider's answer and classify failed ch…
nisargabd Oct 5, 2026
3e278a1
feat: add tokenHeader auth for upstreams that log in and read a bare …
nisargabd Oct 5, 2026
227f6ff
feat: add the Grievance plugin with PMFBY init, confirm and status ma…
nisargabd Oct 5, 2026
7a41690
fix: classify null PMFBY messages and refuse a numeric phone before t…
nisargabd Oct 5, 2026
9e09110
fix: stop logging the provider's body when a mapping rejects its answ…
nisargabd Oct 5, 2026
ca1e14d
refactor: rename exchangeQueryToken to exchangeLoginToken [#57]
nisargabd Oct 5, 2026
0ba5b0d
fix: drop null or empty PMFBY status fields instead of failing or map…
nisargabd Oct 5, 2026
2301196
feat: read a binding from the channel when a payload has no contract …
nisargabd Oct 5, 2026
99c62a8
feat: lodge PMFBY grievances through support and answer errors as des…
nisargabd Oct 5, 2026
9932fc2
refactor: fold the mapping refusal into serve.go [#57]
nisargabd Oct 5, 2026
bec3bd2
fix: name the repeated array and correct comments in the PMFBY grieva…
nisargabd Oct 6, 2026
a4846c4
feat: align the PMFBY grievance replies with the updated design and p…
nisargabd Oct 6, 2026
53894ad
feat: move the PMFBY grievance into its grievance and case bands [#57]
nisargabd Oct 7, 2026
4a558da
feat: read a login token from a nested path in the reply [#57]
nisargabd Oct 8, 2026
5809f0e
feat: drop the PMFBY OTP leg and read FGMS's reply envelope as docume…
nisargabd Oct 8, 2026
3141dfc
fix: let other capabilities' support requests pass the fallback and d…
nisargabd Oct 8, 2026
6e390a2
fix: check the PMFBY phone and description and date a lodge once [#57]
nisargabd Oct 8, 2026
7ef95fd
refactor: read a binding key per action instead of through a fallback…
nisargabd Oct 9, 2026
ecfeb0c
refactor: leave a half-set action block to New's path check [#57]
nisargabd Oct 9, 2026
bf032b7
refactor: list binding paths in order instead of per action [#57]
nisargabd Oct 9, 2026
dc29b94
refactor: keep the login exchange's name and comments as they were [#57]
nisargabd Oct 9, 2026
1f8c153
fix: answer the review on PR #70 [#57]
nisargabd Oct 10, 2026
df3fa3a
feat: follow the grievance pack's code-only case status and orderId b…
nisargabd Oct 10, 2026
fa84699
refactor: shorten the grievance plugin's comments and merge splitPath…
nisargabd Oct 12, 2026
a133fa5
fix: read PMFBY's real no-record reply and refuse another ticket's re…
nisargabd Oct 12, 2026
f75a293
fix: default PMFBY's category pair and read enrolmentId from the chan…
nisargabd Oct 13, 2026
9dec9bb
fix: answer no grievance found as a 202 NACK [#57]
nisargabd Oct 13, 2026
0ec04cc
fix: answer PMFBY refusals with its own message and case times in UTC…
nisargabd Oct 14, 2026
9a1bd84
feat: hold a login token until the expiry its reply states [#57]
nisargabd Oct 14, 2026
f6995b2
fix: hold PMFBY's token until validTo, its real expiry [#57]
nisargabd Oct 14, 2026
4c0a7ce
fix: read PMFBY's InsertDateTime as the RFC 3339 time it sends [#57]
nisargabd Oct 14, 2026
7bf896f
fix: answer PMFBY failures in our own words, its own faults as 502 [#57]
nisargabd Oct 16, 2026
bc350a1
feat: move the PMFBY grievance onto the Grievance pack and return its…
nisargabd Oct 16, 2026
c604cb0
fix: map Open to Registered and date a lodge by when it was lodged [#57]
nisargabd Oct 16, 2026
3dc3018
refactor: leave required fields to the Grievance pack [#57]
nisargabd Oct 19, 2026
c658a54
refactor: name the PMFBY grievance mappings' constants and variables …
nisargabd Oct 22, 2026
d7b7304
fix: reject a blank PMFBY ticket and echo the trimmed description [#57]
nisargabd Oct 22, 2026
4d44c56
refactor: name the status mapping's empty-object helper omitIfEmpty […
nisargabd Oct 22, 2026
141d952
feat: shorten schema errors for listed pack types to the field and fi…
nisargabd Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions config/mappings/pmfby/grievance.status.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# PMFBY status -> POST /krphapi/FGMS/GetGrievenceTicketsStatus.
# Reads a grievance back by ticket number and phone. Only listed fields are
# returned; the farmer's personal details are dropped.
# Required fields and formats are checked by the Grievance pack.

request: |
(
$attributes := beckn.message.contract.commitments[0].commitmentAttributes;
{
"requestorMobileNo": $attributes.applicantMobile,
"GrievenceSupportTicketNo": $attributes.case.ticketNo
}
)

# Empty PMFBY fields are dropped. No record, or one for another ticket, is a
# 400 in our words; any other failure is a 502. PMFBY's message is logged,
# never returned.
response: |
(
/* Constants */
$PMFBY_SUCCESS_CODE := "1";
$SCHEME := { "code": "PMFBY", "name": "Pradhan Mantri Fasal Bima Yojana" };
$INFORMATION_MODE := "Direct";
$COMMITMENT_STATUS_ACTIVE := "ACTIVE";
$DEFAULT_CASE_STATUS := "UnderReview";
$CASE_STATUS_CODES := { "open": "Registered", "resolved": "Resolved" };
$SEASON_NAMES := { "1": "Kharif", "2": "Rabi", "3": "Zaid" };
$TIMESTAMP_FORMAT := "[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01]Z";

/* e.g. "2026-10-12T11:02:49.000Z" */
$RFC3339_PATTERN := /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|[+-][0-9]{2}:[0-9]{2})$/;
$DIGITS_PATTERN := /^[0-9]+$/;
$YEAR_PATTERN := /^[0-9]{4}$/;

$MSG_NOT_FOUND := "No grievance was found for this ticket number and phone number.";
$MSG_UNAVAILABLE := "PMFBY could not return the grievance status right now. Please try again later.";

$ERROR_REFUSED := { "status": 400, "code": "BIZ_GENERIC_ERROR" };
$ERROR_UNAVAILABLE := { "status": 502, "code": "NET_DOWNSTREAM_UNAVAILABLE" };
/* PMFBY fails everything with responseCode "0", so its wording (lower case)
is the only way to tell a caller mistake from a PMFBY fault. */
$KNOWN_REFUSALS := { "no record found": $MSG_NOT_FOUND };

/* Helpers */
/* PMFBY sends null or "" for missing values. */
$isText := function($value) { $type($value) = "string" and $value != "" };
$asText := function($value) { $isText($value) ? $value };
/* Ids come as numbers or digit strings, and 0 means none. */
$asId := function($value) {
($type($value) = "number" or $type($value) = "string")
and $contains($string($value), $DIGITS_PATTERN) and $number($string($value)) != 0
? $string($value)
};
/* Drops an object with no fields instead of sending {}. */
$omitIfEmpty := function($object) { $exists($keys($object)[0]) ? $object };
$error := function($kind, $message, $cause) {
{ "_error": $merge([$kind, { "message": $message, "cause": $cause }]) }
};

/* Inputs */
$commitment := beckn.message.contract.commitments[0];
$attributes := $commitment.commitmentAttributes;
/* [0] reads a bare record or a list of one alike. */
$record := response.responseDynamic[0];
$pmfbyMessage := $type(response.responseMessage) = "string" ? $trim(response.responseMessage);
$refusalMessage := $pmfbyMessage ? $lookup($KNOWN_REFUSALS, $lowercase($pmfbyMessage));
$isAccepted := $string(response.responseCode) = $PMFBY_SUCCESS_CODE;
/* PMFBY reads by phone alone, so a record for another ticket is no record. */
$isOtherTicket := $exists($record.GrievenceSupportTicketNo)
and $string($record.GrievenceSupportTicketNo) != $string($attributes.case.ticketNo);
$isFound := $isAccepted and ($isText($record.ApplicationNo) or $isText($record.TicketStatus)) and $not($isOtherTicket);
$mappedStatus := $isText($record.TicketStatus) ? $lookup($CASE_STATUS_CODES, $lowercase($trim($record.TicketStatus)));
$caseStatus := $exists($mappedStatus) ? $mappedStatus : $DEFAULT_CASE_STATUS;
$requestYear := $asId($record.RequestYear);
$cropYear := $requestYear and $contains($requestYear, $YEAR_PATTERN) ? $requestYear;
$filedOn := $isText($record.InsertDateTime) and $contains($record.InsertDateTime, $RFC3339_PATTERN)
? $fromMillis($toMillis($record.InsertDateTime), $TIMESTAMP_FORMAT);
/* The officer's answer; "" until there is one. */
$reply := $asText($isText($record.latestRemark) ? $trim($record.latestRemark));

/* Output */
$isFound
? {
"context": $merge([beckn.context, { "action": "on_status", "timestamp": $now() }]),
"message": {
"contract": $merge([beckn.message.contract, {
"commitments": [$merge([$commitment, {
"status": { "descriptor": { "code": $COMMITMENT_STATUS_ACTIVE } },
"commitmentAttributes": {
"@context": $attributes.`@context`,
"@type": $attributes.`@type`,
"informationMode": $INFORMATION_MODE,
"scheme": $SCHEME,
"enrolmentId": $asText($record.ApplicationNo),
"cropYear": $cropYear,
"season": $lookup($SEASON_NAMES, $asId($record.RequestSeason)),
"grievance": {
"category": $omitIfEmpty({ "code": $asId($record.TicketCategoryID), "name": $asText($record.TicketCategoryName) }),
"subCategory": $omitIfEmpty({ "code": $asId($record.TicketSubCategoryID), "name": $asText($record.TicketSubCategoryName) }),
"description": $asText($record.GrievenceDescription)
},
"case": {
"ticketNo": $attributes.case.ticketNo,
"status": { "code": $caseStatus },
"filedOn": $filedOn,
"reply": $reply,
"cropName": $asText($record.CropName)
}
}
}])]
}])
}
}
: $isAccepted ? $error($ERROR_REFUSED, $MSG_NOT_FOUND)
: $exists($refusalMessage) ? $error($ERROR_REFUSED, $refusalMessage, $pmfbyMessage)
: $error($ERROR_UNAVAILABLE, $MSG_UNAVAILABLE, $pmfbyMessage)
)
112 changes: 112 additions & 0 deletions config/mappings/pmfby/grievance.support.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# PMFBY support -> POST /krphapi/FGMS/AddKRPHNCIPGrievenceSupportTicket.
# Files a grievance; no OTP. "Grievence" is PMFBY's spelling.
# Required fields and formats are checked by the Grievance pack.
# The two halves cannot share variables, so the default codes are in both.

request: |
(
/* Constants */
$DEFAULT_CATEGORY_CODE := "3";
$DEFAULT_SUB_CATEGORY_CODE := "10";
$DEFAULT_RECEIPT_SOURCE_ID := "134306";
$SEASON_CODES := { "Kharif": "1", "Rabi": "2", "Zaid": "3" };

$DATE_FORMAT := "[Y0001]-[M01]-[D01]";
$IST_OFFSET := "+0530";

/* Helpers */
$orDefault := function($value, $default) { $exists($value) ? $string($value) : $default };

/* Inputs */
$channel := beckn.message.support.channels[0];
$grievance := $channel.grievance;
$filedAt := $exists($channel.complaintDate) ? $toMillis($channel.complaintDate) : $millis();

/* Output */
{
"requestorMobileNo": $channel.applicantMobile,
"applicationNo": $channel.enrolmentId,
"requestYear": $string($channel.cropYear),
"requestSeason": $lookup($SEASON_CODES, $channel.season),
"ticketCategoryID": $orDefault($grievance.category.code, $DEFAULT_CATEGORY_CODE),
"ticketSubCategoryID": $orDefault($grievance.subCategory.code, $DEFAULT_SUB_CATEGORY_CODE),
"grievenceDescription": $trim($grievance.description),
"complaintDate": $fromMillis($filedAt, $DATE_FORMAT, $IST_OFFSET),
"receiptSourceID": $orDefault($channel.receiptSourceId, $DEFAULT_RECEIPT_SOURCE_ID)
}
)

# Echoes the request and adds the ticket. A known PMFBY refusal is a 400 in our
# words; any other failure is a 502. PMFBY's message is logged, never returned.
response: |
(
/* Constants */
$DEFAULT_CATEGORY_CODE := "3";
$DEFAULT_SUB_CATEGORY_CODE := "10";
$PMFBY_SUCCESS_CODE := "1";
$PROVIDER_NAME := "PMFBY Grievance Portal";
$SCHEME := { "code": "PMFBY", "name": "Pradhan Mantri Fasal Bima Yojana" };
$INFORMATION_MODE := "Direct";
$CASE_STATUS_REGISTERED := "Registered";
$TIMESTAMP_FORMAT := "[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01]Z";

$MSG_PHONE_MISMATCH := "The phone number does not match the one registered for this application number.";
$MSG_APPLICATION_NOT_FOUND := "No PMFBY application was found for this application number.";
$MSG_UNAVAILABLE := "PMFBY could not register the grievance right now. Please try again later.";

$ERROR_REFUSED := { "status": 400, "code": "BIZ_GENERIC_ERROR" };
$ERROR_UNAVAILABLE := { "status": 502, "code": "NET_DOWNSTREAM_UNAVAILABLE" };
/* PMFBY fails everything with responseCode "0", so its wording (lower case)
is the only way to tell a caller mistake from a PMFBY fault. */
$KNOWN_REFUSALS := {
"mobile number not matched": $MSG_PHONE_MISMATCH,
"application details not exists": $MSG_APPLICATION_NOT_FOUND
};

/* Helpers */
$orDefault := function($value, $default) { $exists($value) ? $string($value) : $default };
$error := function($kind, $message, $cause) {
{ "_error": $merge([$kind, { "message": $message, "cause": $cause }]) }
};

/* Inputs */
$channel := beckn.message.support.channels[0];
$grievance := $channel.grievance;
$ticketNo := response.responseDynamic.GrievenceSupportTicketNo;
$pmfbyMessage := $type(response.responseMessage) = "string" ? $trim(response.responseMessage);
$refusalMessage := $pmfbyMessage ? $lookup($KNOWN_REFUSALS, $lowercase($pmfbyMessage));
$hasTicket := $type($ticketNo) = "number" or ($type($ticketNo) = "string" and $ticketNo != "");
/* "Ticket Already Raised" also comes back as success with a ticket. */
$isLodged := $string(response.responseCode) = $PMFBY_SUCCESS_CODE and $hasTicket;

/* Output */
$isLodged
? {
"context": $merge([beckn.context, { "action": "on_support", "timestamp": $now() }]),
"message": {
"support": {
"channels": [{
"@context": $channel.`@context`,
"@type": $channel.`@type`,
"informationMode": $INFORMATION_MODE,
"provider": { "id": $channel.provider.id, "descriptor": { "name": $PROVIDER_NAME } },
"scheme": $SCHEME,
"enrolmentId": $channel.enrolmentId,
"grievance": {
"category": { "code": $orDefault($grievance.category.code, $DEFAULT_CATEGORY_CODE) },
"subCategory": { "code": $orDefault($grievance.subCategory.code, $DEFAULT_SUB_CATEGORY_CODE) },
"description": $trim($grievance.description)
},
"case": {
"ticketNo": $string($ticketNo),
"status": { "code": $CASE_STATUS_REGISTERED },
/* Lodge time, so it agrees with what a status read reports. */
"filedOn": $fromMillis($millis(), $TIMESTAMP_FORMAT)
}
}]
}
}
}
: $exists($refusalMessage) ? $error($ERROR_REFUSED, $refusalMessage, $pmfbyMessage)
: $error($ERROR_UNAVAILABLE, $MSG_UNAVAILABLE, $pmfbyMessage)
)
32 changes: 32 additions & 0 deletions config/provider-adapter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ modules:
extendedSchema_maxCacheSize: "100"
extendedSchema_downloadTimeout: "30"

# Pack @types whose errors keep only the first reason, without the
# schema dump, the caller's value or the other branches' reasons.
# Unlisted types keep the full message.
extendedSchema_conciseErrorTypes: "openagrinet:Grievance"

# ------------------------------------------------------------------
# jsonmapper -- the JSONata mapper.
#
Expand Down Expand Up @@ -505,6 +510,32 @@ modules:
# concurrency for this capability.
searchConcurrency: 1

# ------------------------------------------------------------------
# Grievance -- support files a grievance, status reads it back.
# ------------------------------------------------------------------
- id: Grievance
config:
bindingKeys: "pmfby|openagrinet:Grievance"

# Paired by position, tried in order: status names the binding on
# its contract, support on its channel.
providerIdAt: "message.contract.commitments[].offer.provider.id, message.support.channels[].provider.id"
capabilityCodeAt: "message.contract.commitments[].commitmentAttributes.@type, message.support.channels[].@type"

# Logs in at PMFBY's NICUsersLogin and sends the token bare in
# Authorization, held until the login reply's validTo.
# tokenUrl is set per deployment.
pmfby:
authScheme: tokenHeader
tokenUrl: <>
tokenUserField: appAccessUID
tokenUserEnv: PMFBY_ACCESS_UID
tokenSecretField: appAccessPWD
tokenSecretEnv: PMFBY_ACCESS_PWD
tokenResponseField: responseDynamic.token.Token
tokenExpiryField: responseDynamic.token.validTo
headerName: Authorization

# Declaring a step above is not enough: THIS list is what runs. A step
# that appears under providerSteps but not here never executes, and the
# request falls through to the 404 above -- which looks like a registry
Expand All @@ -516,6 +547,7 @@ modules:
- MandiPrice # its binding key, or pass through
- KnowledgeAdvisory # its binding key, or pass through
- AgricultureFacility # its binding key, or pass through
- Grievance # its binding key, or pass through
- signAck # signs whatever the step answered with

# ----------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions install/build-plugins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ plugins=(
"jsonmapper"
"WeatherObservation"
"MandiPrice"
"Grievance"
"KnowledgeAdvisory"
"AgricultureFacility"
"manifestloader"
Expand Down
20 changes: 20 additions & 0 deletions pkg/plugin/implementation/Grievance/Grievance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Package Grievance serves the network's grievance capabilities: filing a
// grievance against a government scheme and checking on it. Provider details
// live in the registry, the mappings and the adapter config, not here.
package Grievance

import (
"context"

"github.com/beckn-one/beckn-onix/pkg/plugin/definition"
"github.com/beckn-one/beckn-onix/pkg/plugin/implementation/internal/common"
)

// Config is the shared step config.
type Config = common.Config

// New creates the grievance step. It has no prerequisites.
func New(ctx context.Context, registry definition.ProviderRecordLookup, mapper definition.Mapper,
cfg *Config) (definition.Step, func() error, error) {
return common.New(ctx, registry, mapper, nil, cfg)
}
Loading
Loading