-
Notifications
You must be signed in to change notification settings - Fork 0
CROSSLINK-183 sync NCIP client #342
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
adamdickmeiss
merged 5 commits into
main
from
CROSSLINK-183-extend-directory-with-ncip-info
Nov 25, 2025
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
e1230d1
CROSSLINK-183 sync NCIP client
adamdickmeiss 553c559
Problem check
adamdickmeiss c9b6e29
consts
adamdickmeiss 667a3b0
Spell fix
adamdickmeiss c663cf0
Merge branch 'main' into CROSSLINK-183-extend-directory-with-ncip-info
adamdickmeiss File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| package ncipclient | ||
|
|
||
| import "github.com/indexdata/crosslink/ncip" | ||
|
|
||
| type NcipMode string | ||
|
|
||
| const ( | ||
| ModeDisabled NcipMode = "disabled" | ||
| ModeAuto NcipMode = "auto" | ||
| ModeManual NcipMode = "manual" | ||
| ) | ||
|
|
||
| type NcipProperty string | ||
|
|
||
| const ( | ||
| Ncip NcipProperty = "ncip" | ||
| FromAgency NcipProperty = "from_agency" | ||
| FromAgencyAuthentication NcipProperty = "from_agency_authentication" | ||
| ToAgency NcipProperty = "to_agency" | ||
| Address NcipProperty = "address" | ||
| LookupUserMode NcipProperty = "lookup_user_mode" | ||
| AcceptItemMode NcipProperty = "accept_item_mode" | ||
| RequestItemMode NcipProperty = "request_item_mode" | ||
| CreateUserFiscalTransactionMode NcipProperty = "create_user_fiscal_transaction_mode" | ||
| ) | ||
|
|
||
| // NcipClient defines the interface for NCIP operations | ||
| // customData is from the DirectoryEntry.CustomData field | ||
| type NcipClient interface { | ||
| // LookupUser performs user authentication. | ||
| // Returns true if authentication is successful (disabled or auto and NCIP lookup succeeded) | ||
| // Returns false if authentication is manual | ||
| // Returns an error otherwise (failed NCIP lookup, misconfiguration, etc) | ||
| LookupUser(customData map[string]any, arg ncip.LookupUser) (bool, error) | ||
|
|
||
| // AcceptItem accepts an item. | ||
| // Returns true if accept is successful (disabled or auto and NCIP accept succeeded) | ||
| // Returns false if accept is manual | ||
| // Returns an error otherwise (failed NCIP accept, misconfiguration, etc) | ||
| AcceptItem(customData map[string]any, arg ncip.AcceptItem) (bool, error) | ||
|
|
||
| DeleteItem(customData map[string]any, arg ncip.DeleteItem) error | ||
|
|
||
| // RequestItem requests an item. | ||
| // Returns true if request is successful (disabled or auto and NCIP request succeeded) | ||
| // Returns false if request is manual | ||
| // Returns an error otherwise (failed NCIP request, misconfiguration, etc) | ||
| RequestItem(customData map[string]any, arg ncip.RequestItem) (bool, error) | ||
|
|
||
| CancelRequestItem(customData map[string]any, arg ncip.CancelRequestItem) error | ||
|
|
||
| CheckInItem(customData map[string]any, arg ncip.CheckInItem) error | ||
|
|
||
| CheckOutItem(customData map[string]any, arg ncip.CheckOutItem) error | ||
|
|
||
| // CreateUserFiscalTransaction creates a user fiscal transaction. | ||
| // Returns true if creation is successful (disabled or auto and NCIP creation succeeded) | ||
| // Returns false if creation is manual | ||
| // Returns an error otherwise (failed NCIP creation, misconfiguration, etc) | ||
| CreateUserFiscalTransaction(customData map[string]any, arg ncip.CreateUserFiscalTransaction) (bool, error) | ||
| } | ||
|
|
||
| type NcipError struct { | ||
| Message string | ||
| Problem ncip.Problem | ||
| } | ||
|
|
||
| func (e *NcipError) Error() string { | ||
| s := e.Message | ||
| if e.Problem.ProblemType.Text != "" { | ||
| s += ": " + e.Problem.ProblemType.Text | ||
| } | ||
| if e.Problem.ProblemDetail != "" { | ||
| s += ": " + e.Problem.ProblemDetail | ||
| } | ||
| return s | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,280 @@ | ||
| package ncipclient | ||
|
|
||
| import ( | ||
| "encoding/xml" | ||
| "fmt" | ||
| "net/http" | ||
|
|
||
| "github.com/indexdata/crosslink/httpclient" | ||
| "github.com/indexdata/crosslink/ncip" | ||
| ) | ||
|
|
||
| type NcipClientImpl struct { | ||
| client *http.Client | ||
| } | ||
|
|
||
| func CreateNcipClient(client *http.Client) NcipClient { | ||
| return &NcipClientImpl{ | ||
| client: client, | ||
| } | ||
| } | ||
|
|
||
| func (n *NcipClientImpl) LookupUser(customData map[string]any, lookup ncip.LookupUser) (bool, error) { | ||
| ncipInfo, err := n.getNcipInfo(customData) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| handle, ret, err := n.checkMode(ncipInfo, LookupUserMode) | ||
| if handle { | ||
| return ret, err | ||
| } | ||
| lookup.InitiationHeader = n.prepareHeader(ncipInfo, lookup.InitiationHeader) | ||
|
|
||
| ncipMessage := &ncip.NCIPMessage{ | ||
| LookupUser: &lookup, | ||
| } | ||
| ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| lookupUserResponse := ncipResponse.LookupUserResponse | ||
| if lookupUserResponse == nil { | ||
| return false, fmt.Errorf("invalid NCIP response: missing LookupUserResponse") | ||
| } | ||
| return true, n.checkProblem("NCIP user lookup", lookupUserResponse.Problem) | ||
| } | ||
|
|
||
| func (n *NcipClientImpl) AcceptItem(customData map[string]any, accept ncip.AcceptItem) (bool, error) { | ||
| ncipInfo, err := n.getNcipInfo(customData) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| handle, ret, err := n.checkMode(ncipInfo, AcceptItemMode) | ||
| if handle { | ||
| return ret, err | ||
| } | ||
| accept.InitiationHeader = n.prepareHeader(ncipInfo, accept.InitiationHeader) | ||
| ncipMessage := &ncip.NCIPMessage{ | ||
| AcceptItem: &accept, | ||
| } | ||
| ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| acceptItemResponse := ncipResponse.AcceptItemResponse | ||
| if acceptItemResponse == nil { | ||
| return false, fmt.Errorf("invalid NCIP response: missing AcceptItemResponse") | ||
| } | ||
| return true, n.checkProblem("NCIP accept item", acceptItemResponse.Problem) | ||
| } | ||
|
|
||
| func (n *NcipClientImpl) DeleteItem(customData map[string]any, delete ncip.DeleteItem) error { | ||
| ncipInfo, err := n.getNcipInfo(customData) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| delete.InitiationHeader = n.prepareHeader(ncipInfo, delete.InitiationHeader) | ||
| ncipMessage := &ncip.NCIPMessage{ | ||
| DeleteItem: &delete, | ||
| } | ||
| ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| deleteItemResponse := ncipResponse.DeleteItemResponse | ||
| if deleteItemResponse == nil { | ||
| return fmt.Errorf("invalid NCIP response: missing DeleteItemResponse") | ||
| } | ||
| return n.checkProblem("NCIP delete item", deleteItemResponse.Problem) | ||
| } | ||
|
|
||
| func (n *NcipClientImpl) RequestItem(customData map[string]any, request ncip.RequestItem) (bool, error) { | ||
| ncipInfo, err := n.getNcipInfo(customData) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| handle, ret, err := n.checkMode(ncipInfo, RequestItemMode) | ||
| if handle { | ||
| return ret, err | ||
| } | ||
| request.InitiationHeader = n.prepareHeader(ncipInfo, request.InitiationHeader) | ||
| ncipMessage := &ncip.NCIPMessage{ | ||
| RequestItem: &request, | ||
| } | ||
| ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| requestItemResponse := ncipResponse.RequestItemResponse | ||
| if requestItemResponse == nil { | ||
| return false, fmt.Errorf("invalid NCIP response: missing RequestItemResponse") | ||
| } | ||
| return true, n.checkProblem("NCIP request item", requestItemResponse.Problem) | ||
| } | ||
|
|
||
| func (n *NcipClientImpl) CancelRequestItem(customData map[string]any, request ncip.CancelRequestItem) error { | ||
| ncipInfo, err := n.getNcipInfo(customData) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| request.InitiationHeader = n.prepareHeader(ncipInfo, request.InitiationHeader) | ||
| ncipMessage := &ncip.NCIPMessage{ | ||
| CancelRequestItem: &request, | ||
| } | ||
| ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| cancelRequestItemResponse := ncipResponse.CancelRequestItemResponse | ||
| if cancelRequestItemResponse == nil { | ||
| return fmt.Errorf("invalid NCIP response: missing CancelRequestItemResponse") | ||
| } | ||
| return n.checkProblem("NCIP cancel request item", cancelRequestItemResponse.Problem) | ||
| } | ||
|
|
||
| func (n *NcipClientImpl) CheckInItem(customData map[string]any, request ncip.CheckInItem) error { | ||
| ncipInfo, err := n.getNcipInfo(customData) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| request.InitiationHeader = n.prepareHeader(ncipInfo, request.InitiationHeader) | ||
| ncipMessage := &ncip.NCIPMessage{ | ||
| CheckInItem: &request, | ||
| } | ||
| ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| checkInItemResponse := ncipResponse.CheckInItemResponse | ||
| if checkInItemResponse == nil { | ||
| return fmt.Errorf("invalid NCIP response: missing CheckInItemResponse") | ||
| } | ||
adamdickmeiss marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return n.checkProblem("NCIP check in item", checkInItemResponse.Problem) | ||
| } | ||
|
|
||
| func (n *NcipClientImpl) CheckOutItem(customData map[string]any, request ncip.CheckOutItem) error { | ||
| ncipInfo, err := n.getNcipInfo(customData) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| request.InitiationHeader = n.prepareHeader(ncipInfo, request.InitiationHeader) | ||
| ncipMessage := &ncip.NCIPMessage{ | ||
| CheckOutItem: &request, | ||
| } | ||
| ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| checkOutItemResponse := ncipResponse.CheckOutItemResponse | ||
| if checkOutItemResponse == nil { | ||
| return fmt.Errorf("invalid NCIP response: missing CheckOutItemResponse") | ||
| } | ||
adamdickmeiss marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return n.checkProblem("NCIP check out item", checkOutItemResponse.Problem) | ||
| } | ||
|
|
||
| func (n *NcipClientImpl) CreateUserFiscalTransaction(customData map[string]any, request ncip.CreateUserFiscalTransaction) (bool, error) { | ||
| ncipInfo, err := n.getNcipInfo(customData) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| handle, ret, err := n.checkMode(ncipInfo, CreateUserFiscalTransactionMode) | ||
| if handle { | ||
| return ret, err | ||
| } | ||
| request.InitiationHeader = n.prepareHeader(ncipInfo, request.InitiationHeader) | ||
|
|
||
| ncipMessage := &ncip.NCIPMessage{ | ||
| CreateUserFiscalTransaction: &request, | ||
| } | ||
| ncipResponse, err := n.sendReceiveMessage(ncipInfo, ncipMessage) | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
| createUserFiscalTransactionResponse := ncipResponse.CreateUserFiscalTransactionResponse | ||
| if createUserFiscalTransactionResponse == nil { | ||
| return false, fmt.Errorf("invalid NCIP response: missing CreateUserFiscalTransactionResponse") | ||
| } | ||
| return true, n.checkProblem("NCIP create user fiscal transaction", createUserFiscalTransactionResponse.Problem) | ||
| } | ||
|
|
||
| func (n *NcipClientImpl) checkProblem(op string, responseProblems []ncip.Problem) error { | ||
| if len(responseProblems) > 0 { | ||
| return &NcipError{ | ||
| Message: op + " failed", | ||
| Problem: responseProblems[0], | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func (n *NcipClientImpl) getNcipInfo(customData map[string]any) (map[string]any, error) { | ||
| ncipInfo, ok := customData["ncip"].(map[string]any) | ||
| if !ok { | ||
| return nil, fmt.Errorf("missing ncip configuration in customData") | ||
| } | ||
| return ncipInfo, nil | ||
| } | ||
|
|
||
| func (n *NcipClientImpl) checkMode(ncipInfo map[string]any, key NcipProperty) (bool, bool, error) { | ||
| mode, ok := ncipInfo[string(key)].(string) | ||
| if !ok { | ||
| return true, false, fmt.Errorf("missing %s in ncip configuration", key) | ||
| } | ||
| switch mode { | ||
| case string(ModeDisabled): | ||
| return true, true, nil | ||
| case string(ModeManual): | ||
| return true, false, nil | ||
| case string(ModeAuto): | ||
| break | ||
| default: | ||
| return true, false, fmt.Errorf("unknown value for %s: %s", key, mode) | ||
| } | ||
| return false, false, nil | ||
| } | ||
|
|
||
| func (n *NcipClientImpl) prepareHeader(ncipInfo map[string]any, header *ncip.InitiationHeader) *ncip.InitiationHeader { | ||
| if header == nil { | ||
| header = &ncip.InitiationHeader{} | ||
| } | ||
| from_agency, ok := ncipInfo[string(FromAgency)].(string) | ||
| if !ok || from_agency == "" { | ||
| from_agency = "default-from-agency" | ||
| } | ||
| header.FromAgencyId.AgencyId = ncip.SchemeValuePair{ | ||
| Text: from_agency, | ||
| } | ||
| to_agency, ok := ncipInfo[string(ToAgency)].(string) | ||
| if !ok || to_agency == "" { | ||
| to_agency = "default-to-agency" | ||
| } | ||
| header.ToAgencyId.AgencyId = ncip.SchemeValuePair{ | ||
| Text: to_agency, | ||
| } | ||
| if auth, ok := ncipInfo[string(FromAgencyAuthentication)].(string); ok { | ||
| header.FromAgencyAuthentication = auth | ||
| } | ||
| return header | ||
| } | ||
|
|
||
| func (n *NcipClientImpl) sendReceiveMessage(ncipInfo map[string]any, message *ncip.NCIPMessage) (*ncip.NCIPMessage, error) { | ||
| url, ok := ncipInfo[string(Address)].(string) | ||
| if !ok { | ||
| return nil, fmt.Errorf("missing NCIP address in customData") | ||
| } | ||
| message.Version = ncip.NCIP_V2_02_XSD | ||
|
|
||
| var respMessage ncip.NCIPMessage | ||
| err := httpclient.NewClient().RequestResponse(n.client, http.MethodPost, []string{httpclient.ContentTypeApplicationXml}, | ||
| url, message, &respMessage, xml.Marshal, xml.Unmarshal) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("NCIP message exchange failed: %s", err.Error()) | ||
| } | ||
| if len(respMessage.Problem) > 0 { | ||
| return nil, &NcipError{ | ||
| Message: "NCIP message processing failed", | ||
| Problem: respMessage.Problem[0], | ||
| } | ||
| } | ||
| return &respMessage, nil | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The RequestItem function does not check for problems in the response. Unlike LookupUser and AcceptItem functions (lines 44-49 and 74-79), this function should check if
requestItemResponse.Problemhas any entries and return an appropriate error. This inconsistency means NCIP errors in RequestItem responses will be silently ignored.