Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 49 additions & 0 deletions config/mappings/pmkisan/application-status.init.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# PM-KISAN application status init -> /ChatbotOTP: send an OTP.

request: |
(
$ca := beckn.message.contract.commitments[0].commitmentAttributes;
$exists($ca.enrolmentId)
? { "Types": "Ben_id", "Values": $ca.enrolmentId }
: { "Types": "Mobile", "Values": $ca.applicant.mobile }
)

# "True" is sent (on_init DRAFT, challenge.issued); anything else is a 502.
response: |
$exists(_status)
? {}
: (
$true := function($v) { $exists($v) and $lowercase($string($v)) = "true" };
$sent := $true(response.Rsponce) or $true(response.Responce) or $true(response.Status);
$commitment := beckn.message.contract.commitments[0];
$ca := $commitment.commitmentAttributes;

$otpWindowMillis := 10 * 60 * 1000;
$not($sent)
? { "_error": { "status": 502, "code": "NET_DOWNSTREAM_UNAVAILABLE",
"message": "PM-KISAN could not send the OTP" } }
: {
"context": $merge([beckn.context, { "action": "on_init", "timestamp": $now() }]),
"message": {
"contract": $merge([beckn.message.contract, {
"commitments": [$merge([$commitment, {
"status": { "descriptor": { "code": "DRAFT" } },
"commitmentAttributes": {
"@context": $ca.`@context`,
"@type": $ca.`@type`,
"informationMode": "OnDemand",
"scheme": $ca.scheme,
"challenge": {
"issued": {
"method": "SMS_OTP",
"length": 4,
"valueType": "NUMERIC",
"expiresAt": $fromMillis($millis() + $otpWindowMillis)
}
}
}
}])]
}])
}
}
)
109 changes: 109 additions & 0 deletions config/mappings/pmkisan/application-status.status.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# PM-KISAN application status -> /ChatbotUserDetails, then the blockers (ADR-0005).

# The OTP is the one check left here (ADR-0003).
request: |
(
$ca := beckn.message.contract.commitments[0].commitmentAttributes;
$not($exists($ca.challenge.response.value))
? { "_error": { "status": 400, "code": "SCH_REQUIRED_FIELD_MISSING",
"message": "challenge.response.value is required",
"path": "$.message.contract.commitments[0].commitmentAttributes.challenge.response.value" } }
: $exists($ca.enrolmentId)
? { "Types": "Ben_id", "Values": $ca.enrolmentId }
: { "Types": "Mobile", "Values": $ca.applicant.mobile }
)

# "False": 400; no "True" or no instalment count: 502. Only allow-listed fields are returned.
response: |
$exists(_status)
? {}
: (
$flag := function($r) {
$exists($r.Rsponce) ? $lowercase($string($r.Rsponce))
: $exists($r.Responce) ? $lowercase($string($r.Responce))
: $exists($r.Status) ? $lowercase($string($r.Status))
};
$text := function($v) {
$type($v) = "string" and $length($trim($v)) > 0 ? $trim($v)
};
$count0 := (
$n := response.LatestInstallmentPaid;
$type($n) = "number" ? $n
: $type($n) = "string" and $exists($match($trim($n), /^[0-9]+$/)[0]) ? $number($trim($n))
);

/* ISO, /Date(ms)/ or M/D/YYYY, as IST; anything else is left out. */
$istPicture := "[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01][Z01:01]";
$pad := function($s) { $length($s) = 1 ? "0" & $s : $s };
$registeredOn := (
$d := $text(response.DateOfRegistration);
$exists($match($d, /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}/)[0])
? $substring($d, 0, 10) & "T" & $substring($d, 11, 8) & "+05:30"
: $exists($match($d, /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/)[0])
? $d & "T00:00:00+05:30"
: $exists($match($d, /^\/Date\([0-9]+([+-][0-9]{4})?\)\/$/)[0])
? $fromMillis($number($split($substring($d, 6), /[^0-9]/)[0]), $istPicture, "+0530")
: $exists($match($d, /^[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{4}/)[0])
? (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we need this complex extraction logic?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. The four formats were a guess, because we hadn't seen a real DateOfRegistration I'll check a real staging response and keep only that format, leaving the date out if it doesn't match.

$mdy := $split($split($d, " ")[0], "/");
$mdy[2] & "-" & $pad($mdy[0]) & "-" & $pad($mdy[1]) & "T00:00:00+05:30"
)
);

$bs := response._beneficiaryStatus;
$governed := { "income tax payee": "IncomeTaxPayee", "land seeding, kys": "LandSeedingPending" };
$notBlockers := ["Rsponce", "Responce", "Status", "status", "Message", "message"];
$blockers := $flag($bs) = "true"
? (
$phrases := $distinct($filter(
$map($sort($filter($keys($bs), function($k) { $not($k in $notBlockers) })),
function($k) { $text($lookup($bs, $k)) }),
function($p) { $exists($p) and $lowercase($p) != "no errors" }));
[$map($phrases, function($p) {
{
"code": (
$known := $lookup($governed, $lowercase($p));
$exists($known) ? $known : "Other"
),
"name": $p
}
})]
);

$c := beckn.message.contract.commitments[0];
$ca := $c.commitmentAttributes;

$flag(response) = "false"
? { "_error": { "status": 400, "code": "BIZ_GENERIC_ERROR",
"message": "PM-KISAN holds no registration for this applicant." } }
: $flag(response) != "true" or $not($exists($count0))
? { "_error": { "status": 502, "code": "NET_DOWNSTREAM_UNAVAILABLE",
"message": "PM-KISAN could not read the registration" } }
: {
"context": $merge([beckn.context, { "action": "on_status", "timestamp": $now() }]),
"message": {
"contract": {
"id": beckn.message.contract.id,
"commitments": [{
"status": { "descriptor": { "code": "CLOSED" } },
"offer": $c.offer,
"resources": $c.resources,
"commitmentAttributes": {
"@context": $ca.`@context`,
"@type": $ca.`@type`,
"informationMode": "Direct",
"scheme": $ca.scheme,
"enrolmentId": $text(response.Reg_No),
"application": {
"registeredOn": $registeredOn,
"latestInstalmentPaid": $count0,
"ekyc": $exists(response.eKYC_Status)
? { "code": $uppercase($trim($string(response.eKYC_Status))) = "Y" ? "Done" : "Pending" },
"blockers": $blockers
}
}
}]
}
}
}
)
19 changes: 18 additions & 1 deletion config/provider-adapter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -511,10 +511,11 @@ modules:
# ------------------------------------------------------------------
# Grievance -- support files a grievance, status reads it back.
# PM-KISAN: init (OTP), then support or status with the OTP.
# PM-KISAN application status: init (OTP), then status.
# ------------------------------------------------------------------
- id: Grievance
config:
bindingKeys: "pmfby|openagrinet:Grievance,pmkisan|openagrinet:Grievance"
bindingKeys: "pmfby|openagrinet:Grievance,pmkisan|openagrinet:Grievance,pmkisan-chatbot|openagrinet:SchemeApplication"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it a chatbot? Can we keep it as PMKisan only?


# Paired by position, tried in order: status names the binding on
# its contract, support on its channel.
Expand Down Expand Up @@ -556,6 +557,22 @@ modules:
otpVerifyActions: "support,status"
otpVerifyTimeoutMs: "10000"

# PM-KISAN application status: the chatbot service, with a follow-up read (ADR-0005).
pmkisan-chatbot:
authScheme: none
envelope: aesCbcKeyInBand
envelopeTokenEnv: PMKISAN_OTP_TOKEN
envelopeTokenField: Token
envelopeRequestField: EncryptedRequest
envelopeResponsePaths: "d.output,output"
otpVerifyPath: /ChatbotOTPVerified
otpVerifyActions: status
otpVerifyTimeoutMs: "10000"
followUpPath: /ChatbotBeneficiaryStatus
followUpField: _beneficiaryStatus
followUpActions: status
followUpTimeoutMs: "10000"

# 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 Down
Loading
Loading