diff --git a/api/members/members_client/learning_contract_test.go b/api/members/members_client/learning_contract_test.go index 7e6dd44..5f872fc 100644 --- a/api/members/members_client/learning_contract_test.go +++ b/api/members/members_client/learning_contract_test.go @@ -23,7 +23,7 @@ import ( "github.com/go-openapi/strfmt" ) -const membersProviderSpecSHA256 = "631ed1313bf3a2fee7d7f1cd053c09b058bf8acb6117d3100af24e340c57291e" +const membersProviderSpecSHA256 = "6f60547e3faf342bf4b9f6aad15eebfde0cd553f60550f67d71e469dbd83cc7f" var learningTestAuth = openapiruntime.ClientAuthInfoWriterFunc(func(openapiruntime.ClientRequest, strfmt.Registry) error { return nil @@ -517,7 +517,7 @@ func TestMembersSpecIsPinnedToProviderSource(t *testing.T) { } sum := sha256.Sum256(mainSpec) if got := hex.EncodeToString(sum[:]); got != membersProviderSpecSHA256 { - t.Fatalf("Members spec drifted from the governed TrackEvent provider source: SHA-256 = %s, want %s", got, membersProviderSpecSHA256) + t.Fatalf("Members spec drifted from the integrated TrackEvent and Transaction provider contract: SHA-256 = %s, want %s", got, membersProviderSpecSHA256) } externalSpec, err := os.ReadFile(filepath.Join(repoRoot, "swagger", "external", "members-vernonkeenan.yaml")) diff --git a/api/members/members_client/track_contract_test.go b/api/members/members_client/track_contract_test.go index dbce353..48a818d 100644 --- a/api/members/members_client/track_contract_test.go +++ b/api/members/members_client/track_contract_test.go @@ -19,7 +19,7 @@ import ( "github.com/go-openapi/strfmt" ) -const membersTrackSpecSHA256 = "631ed1313bf3a2fee7d7f1cd053c09b058bf8acb6117d3100af24e340c57291e" +const membersTrackSpecSHA256 = "6f60547e3faf342bf4b9f6aad15eebfde0cd553f60550f67d71e469dbd83cc7f" var trackTestAuth = openapiruntime.ClientAuthInfoWriterFunc( func(openapiruntime.ClientRequest, strfmt.Registry) error { return nil }, @@ -228,7 +228,7 @@ func TestMembersSpecIsPinnedToTrackProviderCommit(t *testing.T) { sum := sha256.Sum256(mainSpec) if got := hex.EncodeToString(sum[:]); got != membersTrackSpecSHA256 { t.Fatalf( - "Members spec drifted from the governed TrackEvent provider source: SHA-256 = %s, want %s", + "Members spec drifted from the integrated TrackEvent and Transaction provider contract: SHA-256 = %s, want %s", got, membersTrackSpecSHA256, ) } diff --git a/api/members/members_client/transaction_contract_test.go b/api/members/members_client/transaction_contract_test.go new file mode 100644 index 0000000..cb0d46d --- /dev/null +++ b/api/members/members_client/transaction_contract_test.go @@ -0,0 +1,243 @@ +package members_client_test + +import ( + "context" + "os" + "path/filepath" + "reflect" + "regexp" + "strings" + "testing" + + "code.tnxs.net/vernonkeenan/lib/api/members/members_client" + "code.tnxs.net/vernonkeenan/lib/api/members/members_client/transactions" + "code.tnxs.net/vernonkeenan/lib/api/members/members_models" + openapiruntime "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" +) + +type transactionCaptureTransport struct { + operation *openapiruntime.ClientOperation +} + +func (transport *transactionCaptureTransport) Submit( + operation *openapiruntime.ClientOperation, +) (any, error) { + return transport.SubmitContext(context.Background(), operation) +} + +func (transport *transactionCaptureTransport) SubmitContext( + _ context.Context, + operation *openapiruntime.ClientOperation, +) (any, error) { + transport.operation = operation + switch operation.ID { + case "getTransactions": + return transactions.NewGetTransactionsOK(), nil + case "postTransactions": + return transactions.NewPostTransactionsOK(), nil + case "putTransactions": + return transactions.NewPutTransactionsOK(), nil + default: + panic("unexpected generated Transaction operation: " + operation.ID) + } +} + +func TestGeneratedTransactionOperationContract(t *testing.T) { + tenantID := strfmt.UUID("55555555-5555-4555-8555-555555555555") + auth := openapiruntime.ClientAuthInfoWriterFunc( + func(openapiruntime.ClientRequest, strfmt.Registry) error { return nil }, + ) + type operationCall func(openapiruntime.ContextualTransport) error + tests := []struct { + name, wantID, wantMethod string + call operationCall + }{ + { + name: "list or get", wantID: "getTransactions", wantMethod: "GET", + call: func(transport openapiruntime.ContextualTransport) error { + _, err := transactions.New(transport, strfmt.Default).GetTransactions( + transactions.NewGetTransactionsParams().WithTenantID(tenantID), auth, + ) + return err + }, + }, + { + name: "create", wantID: "postTransactions", wantMethod: "POST", + call: func(transport openapiruntime.ContextualTransport) error { + _, err := transactions.New(transport, strfmt.Default).PostTransactions( + transactions.NewPostTransactionsParams().WithTenantID(tenantID), auth, + ) + return err + }, + }, + { + name: "CAS transition", wantID: "putTransactions", wantMethod: "PUT", + call: func(transport openapiruntime.ContextualTransport) error { + _, err := transactions.New(transport, strfmt.Default).PutTransactions( + transactions.NewPutTransactionsParams().WithTenantID(tenantID), auth, + ) + return err + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + transport := &transactionCaptureTransport{} + if err := test.call(transport); err != nil { + t.Fatalf("generated client operation failed: %v", err) + } + if transport.operation == nil { + t.Fatal("generated client did not submit an operation") + } + if transport.operation.ID != test.wantID || + transport.operation.Method != test.wantMethod || + transport.operation.PathPattern != "/transactions" { + t.Fatalf( + "operation = %s %s %s, want %s %s /transactions", + transport.operation.ID, transport.operation.Method, + transport.operation.PathPattern, test.wantID, test.wantMethod, + ) + } + if transport.operation.AuthInfo == nil { + t.Fatal("generated operation discarded its compound auth writer") + } + }) + } +} + +func TestTransactionTenantBindingExactScopesAndCAS(t *testing.T) { + specText := readMembersLearningSpec(t) + pathBlock := learningSpecPathBlock(t, specText, "/transactions") + for _, forbidden := range []string{" delete:", " patch:"} { + if strings.Contains(pathBlock, forbidden) { + t.Errorf("/transactions exposes unsupported %s", strings.TrimSpace(forbidden)) + } + } + operations := map[string]struct{ id, scope string }{ + "get": {id: "getTransactions", scope: "members:transaction:read"}, + "post": {id: "postTransactions", scope: "members:transaction:create"}, + "put": {id: "putTransactions", scope: "members:transaction:update"}, + } + const compoundSecurity = " security:\n - ApiKeyAuth: []\n kvSessionCookie: []" + for method, expected := range operations { + block := learningSpecOperationBlock(t, specText, "/transactions", method) + if !strings.Contains(block, " operationId: "+expected.id+"\n") { + t.Errorf("%s /transactions lost operationId %q", strings.ToUpper(method), expected.id) + } + if strings.Count(block, compoundSecurity) != 1 { + t.Errorf("%s /transactions does not preserve compound authentication", strings.ToUpper(method)) + } + if !strings.Contains(block, expected.scope) { + t.Errorf("%s /transactions lost exact scope %q", strings.ToUpper(method), expected.scope) + } + if !strings.Contains(block, `- $ref: "#/parameters/transactionTenantId"`) { + t.Errorf("%s /transactions is not tenantId-bound", strings.ToUpper(method)) + } + } + + tenantID := strfmt.UUID("55555555-5555-4555-8555-555555555555") + recordID := strfmt.UUID("66666666-6666-4666-8666-666666666666") + get := transactions.NewGetTransactionsParams(). + WithTenantID(tenantID). + WithTransactionID(&recordID) + if get.TenantID != tenantID || get.TransactionID == nil || + *get.TransactionID != recordID { + t.Fatal("Transaction list/get client lost its tenant or record filter") + } + if transactions.NewPostTransactionsParams().WithTenantID(tenantID).TenantID != tenantID { + t.Fatal("Transaction create client lost required tenantId") + } + if transactions.NewPutTransactionsParams().WithTenantID(tenantID).TenantID != tenantID { + t.Fatal("Transaction update client lost required tenantId") + } + if !transactions.NewPutTransactionsConflict().IsCode(409) { + t.Fatal("Transaction update client lost optimistic concurrency response") + } +} + +func TestTransactionModelIsExactDecimalAndPaymentTokenFree(t *testing.T) { + assertExactModelFields(t, members_models.Transaction{}, map[string]string{ + "Amount": "string", "CreatedByID": "*strfmt.UUID", + "CreatedDate": "*strfmt.DateTime", "Currency": "string", + "ID": "strfmt.UUID", "LastModifiedByID": "*strfmt.UUID", + "LastModifiedDate": "*strfmt.DateTime", "Status": "*string", + "TenantID": "*strfmt.UUID", "TransactionDate": "*strfmt.DateTime", + }) + + valid := &members_models.TransactionRequest{Data: []*members_models.Transaction{{ + Amount: "10.25", Currency: "USD", + }}} + if err := valid.Validate(strfmt.Default); err != nil { + t.Fatalf("generated Transaction request rejected one valid record: %v", err) + } + valid.Data = append(valid.Data, &members_models.Transaction{Amount: "11.00", Currency: "USD"}) + if err := valid.Validate(strfmt.Default); err == nil { + t.Fatal("generated Transaction request accepted a bulk payload") + } +} + +func TestTransactionSurfaceHasNoDeleteOrProtectedRelationshipMethods(t *testing.T) { + contract := reflect.TypeOf((*transactions.ClientService)(nil)).Elem() + for index := 0; index < contract.NumMethod(); index++ { + method := contract.Method(index).Name + for _, forbidden := range []string{ + "Delete", "Payment", "Processor", "Database", "DSN", + } { + if strings.Contains(method, forbidden) { + t.Errorf("Transaction client exposes forbidden method %s", method) + } + } + } + root := reflect.TypeOf(members_client.Members{}) + if _, exists := root.FieldByName("Transactions"); !exists { + t.Fatal("root Members client does not expose governed Transactions") + } +} + +func TestGeneratedTransactionSurfaceHasNoExternalBypassOrSecrets(t *testing.T) { + repoRoot := learningRepoRoot(t) + targets := []string{ + filepath.Join(repoRoot, "api", "members", "members_client", "transactions"), + filepath.Join(repoRoot, "api", "members", "members_models", "transaction.go"), + filepath.Join(repoRoot, "api", "members", "members_models", "transaction_request.go"), + filepath.Join(repoRoot, "api", "members", "members_models", "transaction_response.go"), + } + credentialLiteral := regexp.MustCompile( + `(?i)(api[_-]?key|password|secret)\s*[:=]\s*["'][^"']+["']`, + ) + for _, target := range targets { + err := filepath.WalkDir(target, func( + path string, entry os.DirEntry, walkErr error, + ) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() || !strings.HasSuffix(path, ".go") || + strings.HasSuffix(path, "_test.go") { + return nil + } + content, err := os.ReadFile(path) + if err != nil { + return err + } + lower := strings.ToLower(string(content)) + for _, forbidden := range []string{ + "salesforce", "sf-gate", "go-force", "private key-----", + "members_client/databases", "processortoken", "dsn", + } { + if strings.Contains(lower, forbidden) { + t.Errorf("%s contains forbidden boundary %q", path, forbidden) + } + } + if credentialLiteral.Match(content) { + t.Errorf("%s contains a credential-shaped literal", path) + } + return nil + }) + if err != nil { + t.Fatalf("scan generated Transaction target %s: %v", target, err) + } + } +} diff --git a/api/members/members_client/transactions/get_transactions_parameters.go b/api/members/members_client/transactions/get_transactions_parameters.go index a79e378..efe9b19 100644 --- a/api/members/members_client/transactions/get_transactions_parameters.go +++ b/api/members/members_client/transactions/get_transactions_parameters.go @@ -67,11 +67,6 @@ GetTransactionsParams contains all the parameters to send to the API endpoint */ type GetTransactionsParams struct { - // ID. - // - // Unique Record ID - ID *string - // Limit. // // How many objects to return at one time @@ -86,6 +81,20 @@ type GetTransactionsParams struct { // Format: int64 Offset *int64 + // TenantID. + // + // Tenant that owns the Transaction. Members verifies the active human membership and never accepts TenantID from the body. + // + // Format: uuid + TenantID strfmt.UUID + + // TransactionID. + // + // Record Id of a governed Transaction + // + // Format: uuid + TransactionID *strfmt.UUID + HTTPClient *http.Client inner innerParams @@ -143,17 +152,6 @@ func (o *GetTransactionsParams) SetHTTPClient(client *http.Client) { o.HTTPClient = client } -// WithID adds the id to the get transactions params. -func (o *GetTransactionsParams) WithID(id *string) *GetTransactionsParams { - o.SetID(id) - return o -} - -// SetID adds the id to the get transactions params. -func (o *GetTransactionsParams) SetID(id *string) { - o.ID = id -} - // WithLimit adds the limit to the get transactions params. func (o *GetTransactionsParams) WithLimit(limit *int64) *GetTransactionsParams { o.SetLimit(limit) @@ -176,6 +174,28 @@ func (o *GetTransactionsParams) SetOffset(offset *int64) { o.Offset = offset } +// WithTenantID adds the tenantID to the get transactions params. +func (o *GetTransactionsParams) WithTenantID(tenantID strfmt.UUID) *GetTransactionsParams { + o.SetTenantID(tenantID) + return o +} + +// SetTenantID adds the tenantId to the get transactions params. +func (o *GetTransactionsParams) SetTenantID(tenantID strfmt.UUID) { + o.TenantID = tenantID +} + +// WithTransactionID adds the transactionID to the get transactions params. +func (o *GetTransactionsParams) WithTransactionID(transactionID *strfmt.UUID) *GetTransactionsParams { + o.SetTransactionID(transactionID) + return o +} + +// SetTransactionID adds the transactionId to the get transactions params. +func (o *GetTransactionsParams) SetTransactionID(transactionID *strfmt.UUID) { + o.TransactionID = transactionID +} + // WriteToRequest writes these params to a [runtime.ClientRequest]. func (o *GetTransactionsParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { if err := r.SetTimeout(o.inner.timeout); err != nil { @@ -183,23 +203,6 @@ func (o *GetTransactionsParams) WriteToRequest(r runtime.ClientRequest, reg strf } var res []error - if o.ID != nil { - - // query param id - var qrID string - - if o.ID != nil { - qrID = *o.ID - } - qID := qrID - if qID != "" { - - if err := r.SetQueryParam("id", qID); err != nil { - return err - } - } - } - if o.Limit != nil { // query param limit @@ -234,6 +237,33 @@ func (o *GetTransactionsParams) WriteToRequest(r runtime.ClientRequest, reg strf } } + // query param tenantId + qrTenantID := o.TenantID + qTenantID := qrTenantID.String() + if qTenantID != "" { + + if err := r.SetQueryParam("tenantId", qTenantID); err != nil { + return err + } + } + + if o.TransactionID != nil { + + // query param transactionId + var qrTransactionID strfmt.UUID + + if o.TransactionID != nil { + qrTransactionID = *o.TransactionID + } + qTransactionID := qrTransactionID.String() + if qTransactionID != "" { + + if err := r.SetQueryParam("transactionId", qTransactionID); err != nil { + return err + } + } + } + if len(res) > 0 { return errors.CompositeValidationError(res...) } diff --git a/api/members/members_client/transactions/post_transactions_parameters.go b/api/members/members_client/transactions/post_transactions_parameters.go index cde60a2..63a8fbe 100644 --- a/api/members/members_client/transactions/post_transactions_parameters.go +++ b/api/members/members_client/transactions/post_transactions_parameters.go @@ -67,6 +67,13 @@ PostTransactionsParams contains all the parameters to send to the API endpoint */ type PostTransactionsParams struct { + // TenantID. + // + // Tenant that owns the Transaction. Members verifies the active human membership and never accepts TenantID from the body. + // + // Format: uuid + TenantID strfmt.UUID + // TransactionRequest. // // An array of new Transaction records @@ -129,6 +136,17 @@ func (o *PostTransactionsParams) SetHTTPClient(client *http.Client) { o.HTTPClient = client } +// WithTenantID adds the tenantID to the post transactions params. +func (o *PostTransactionsParams) WithTenantID(tenantID strfmt.UUID) *PostTransactionsParams { + o.SetTenantID(tenantID) + return o +} + +// SetTenantID adds the tenantId to the post transactions params. +func (o *PostTransactionsParams) SetTenantID(tenantID strfmt.UUID) { + o.TenantID = tenantID +} + // WithTransactionRequest adds the transactionRequest to the post transactions params. func (o *PostTransactionsParams) WithTransactionRequest(transactionRequest *members_models.TransactionRequest) *PostTransactionsParams { o.SetTransactionRequest(transactionRequest) @@ -146,6 +164,16 @@ func (o *PostTransactionsParams) WriteToRequest(r runtime.ClientRequest, reg str return err } var res []error + + // query param tenantId + qrTenantID := o.TenantID + qTenantID := qrTenantID.String() + if qTenantID != "" { + + if err := r.SetQueryParam("tenantId", qTenantID); err != nil { + return err + } + } if o.TransactionRequest != nil { if err := r.SetBodyParam(o.TransactionRequest); err != nil { return err diff --git a/api/members/members_client/transactions/put_transactions_parameters.go b/api/members/members_client/transactions/put_transactions_parameters.go index 88ef4b1..2bcd6c3 100644 --- a/api/members/members_client/transactions/put_transactions_parameters.go +++ b/api/members/members_client/transactions/put_transactions_parameters.go @@ -67,6 +67,13 @@ PutTransactionsParams contains all the parameters to send to the API endpoint */ type PutTransactionsParams struct { + // TenantID. + // + // Tenant that owns the Transaction. Members verifies the active human membership and never accepts TenantID from the body. + // + // Format: uuid + TenantID strfmt.UUID + // TransactionRequest. // // An array of new Transaction records @@ -129,6 +136,17 @@ func (o *PutTransactionsParams) SetHTTPClient(client *http.Client) { o.HTTPClient = client } +// WithTenantID adds the tenantID to the put transactions params. +func (o *PutTransactionsParams) WithTenantID(tenantID strfmt.UUID) *PutTransactionsParams { + o.SetTenantID(tenantID) + return o +} + +// SetTenantID adds the tenantId to the put transactions params. +func (o *PutTransactionsParams) SetTenantID(tenantID strfmt.UUID) { + o.TenantID = tenantID +} + // WithTransactionRequest adds the transactionRequest to the put transactions params. func (o *PutTransactionsParams) WithTransactionRequest(transactionRequest *members_models.TransactionRequest) *PutTransactionsParams { o.SetTransactionRequest(transactionRequest) @@ -146,6 +164,16 @@ func (o *PutTransactionsParams) WriteToRequest(r runtime.ClientRequest, reg strf return err } var res []error + + // query param tenantId + qrTenantID := o.TenantID + qTenantID := qrTenantID.String() + if qTenantID != "" { + + if err := r.SetQueryParam("tenantId", qTenantID); err != nil { + return err + } + } if o.TransactionRequest != nil { if err := r.SetBodyParam(o.TransactionRequest); err != nil { return err diff --git a/api/members/members_client/transactions/put_transactions_responses.go b/api/members/members_client/transactions/put_transactions_responses.go index 2be1631..85729f6 100644 --- a/api/members/members_client/transactions/put_transactions_responses.go +++ b/api/members/members_client/transactions/put_transactions_responses.go @@ -49,6 +49,12 @@ func (o *PutTransactionsReader) ReadResponse(response runtime.ClientResponse, co return nil, err } return nil, result + case 409: + result := NewPutTransactionsConflict() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result case 422: result := NewPutTransactionsUnprocessableEntity() if err := result.readResponse(response, consumer, o.formats); err != nil { @@ -347,6 +353,74 @@ func (o *PutTransactionsNotFound) readResponse(response runtime.ClientResponse, return nil } +// NewPutTransactionsConflict creates a PutTransactionsConflict with default headers values +func NewPutTransactionsConflict() *PutTransactionsConflict { + return &PutTransactionsConflict{} +} + +// PutTransactionsConflict describes a response with status code 409, with default header values. +// +// The supplied LastModifiedDate is stale; reload the record before retrying. +type PutTransactionsConflict struct { + Payload *members_models.Error +} + +// IsSuccess returns true when this put transactions conflict response has a 2xx status code +func (o *PutTransactionsConflict) IsSuccess() bool { + return false +} + +// IsRedirect returns true when this put transactions conflict response has a 3xx status code +func (o *PutTransactionsConflict) IsRedirect() bool { + return false +} + +// IsClientError returns true when this put transactions conflict response has a 4xx status code +func (o *PutTransactionsConflict) IsClientError() bool { + return true +} + +// IsServerError returns true when this put transactions conflict response has a 5xx status code +func (o *PutTransactionsConflict) IsServerError() bool { + return false +} + +// IsCode returns true when this put transactions conflict response a status code equal to that given +func (o *PutTransactionsConflict) IsCode(code int) bool { + return code == 409 +} + +// Code gets the status code for the put transactions conflict response +func (o *PutTransactionsConflict) Code() int { + return 409 +} + +func (o *PutTransactionsConflict) Error() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /transactions][%d] putTransactionsConflict %s", 409, payload) +} + +func (o *PutTransactionsConflict) String() string { + payload, _ := json.Marshal(o.Payload) + return fmt.Sprintf("[PUT /transactions][%d] putTransactionsConflict %s", 409, payload) +} + +func (o *PutTransactionsConflict) GetPayload() *members_models.Error { + return o.Payload +} + +func (o *PutTransactionsConflict) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(members_models.Error) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) { + return err + } + + return nil +} + // NewPutTransactionsUnprocessableEntity creates a PutTransactionsUnprocessableEntity with default headers values func NewPutTransactionsUnprocessableEntity() *PutTransactionsUnprocessableEntity { return &PutTransactionsUnprocessableEntity{} diff --git a/api/members/members_client/transactions/transactions_client.go b/api/members/members_client/transactions/transactions_client.go index 2bbba60..4497351 100644 --- a/api/members/members_client/transactions/transactions_client.go +++ b/api/members/members_client/transactions/transactions_client.go @@ -60,30 +60,30 @@ type ClientOption func(*runtime.ClientOperation) // ClientService is the interface for Client methods. type ClientService interface { - // GetTransactions get a list of transactions. + // GetTransactions get governed tenant transactions. GetTransactions(params *GetTransactionsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetTransactionsOK, error) - // GetTransactionsContext get a list of transactions. + // GetTransactionsContext get governed tenant transactions. GetTransactionsContext(ctx context.Context, params *GetTransactionsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetTransactionsOK, error) - // PostTransactions create new transactions. + // PostTransactions create a governed transaction. PostTransactions(params *PostTransactionsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PostTransactionsOK, error) - // PostTransactionsContext create new transactions. + // PostTransactionsContext create a governed transaction. PostTransactionsContext(ctx context.Context, params *PostTransactionsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PostTransactionsOK, error) - // PutTransactions update transaction. + // PutTransactions transition a governed transaction. PutTransactions(params *PutTransactionsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PutTransactionsOK, error) - // PutTransactionsContext update transaction. + // PutTransactionsContext transition a governed transaction. PutTransactionsContext(ctx context.Context, params *PutTransactionsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PutTransactionsOK, error) SetTransport(transport runtime.ContextualTransport) } -// GetTransactions gets a list of transactions. +// GetTransactions gets governed tenant transactions. // -// Return a list of Transaction records from the datastore. +// Return tenant-scoped, payment-token-free Transaction records. Requires members:transaction:read and active Owner or Manager access in tenantId.. // // This method does not support injected context. // However, timeout and opentracing contexts are honored whenever enabled. @@ -100,9 +100,9 @@ func (a *Client) GetTransactions(params *GetTransactionsParams, authInfo runtime return a.GetTransactionsContext(ctx, params, authInfo, opts...) } -// GetTransactionsContext gets a list of transactions. +// GetTransactionsContext gets governed tenant transactions. // -// Return a list of Transaction records from the datastore. +// Return tenant-scoped, payment-token-free Transaction records. Requires members:transaction:read and active Owner or Manager access in tenantId.. // // Do not use the deprecated [GetTransactionsParams.Context] with this method: it would be ignored. func (a *Client) GetTransactionsContext(ctx context.Context, params *GetTransactionsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*GetTransactionsOK, error) { @@ -148,9 +148,9 @@ func (a *Client) GetTransactionsContext(ctx context.Context, params *GetTransact panic(msg) } -// PostTransactions creates new transactions. +// PostTransactions creates a governed transaction. // -// Create Transactions. +// Create one tenant-scoped Transaction in pending state. Requires members:transaction:create and active Owner or Manager access. IDs, tenant ownership, lifecycle state, audit fields, and all payment-method references are server-owned.. // // This method does not support injected context. // However, timeout and opentracing contexts are honored whenever enabled. @@ -167,9 +167,9 @@ func (a *Client) PostTransactions(params *PostTransactionsParams, authInfo runti return a.PostTransactionsContext(ctx, params, authInfo, opts...) } -// PostTransactionsContext creates new transactions. +// PostTransactionsContext creates a governed transaction. // -// Create Transactions. +// Create one tenant-scoped Transaction in pending state. Requires members:transaction:create and active Owner or Manager access. IDs, tenant ownership, lifecycle state, audit fields, and all payment-method references are server-owned.. // // Do not use the deprecated [PostTransactionsParams.Context] with this method: it would be ignored. func (a *Client) PostTransactionsContext(ctx context.Context, params *PostTransactionsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PostTransactionsOK, error) { @@ -215,9 +215,9 @@ func (a *Client) PostTransactionsContext(ctx context.Context, params *PostTransa panic(msg) } -// PutTransactions updates transaction. +// PutTransactions transitions a governed transaction. // -// Update Transaction. +// CAS-transition one tenant-scoped Transaction. Requires members:transaction:update, active Owner or Manager access, and the current LastModifiedDate. Amount, currency, transaction date, tenant ownership, relationships, audit fields, and payment-method references are immutable.. // // This method does not support injected context. // However, timeout and opentracing contexts are honored whenever enabled. @@ -234,9 +234,9 @@ func (a *Client) PutTransactions(params *PutTransactionsParams, authInfo runtime return a.PutTransactionsContext(ctx, params, authInfo, opts...) } -// PutTransactionsContext updates transaction. +// PutTransactionsContext transitions a governed transaction. // -// Update Transaction. +// CAS-transition one tenant-scoped Transaction. Requires members:transaction:update, active Owner or Manager access, and the current LastModifiedDate. Amount, currency, transaction date, tenant ownership, relationships, audit fields, and payment-method references are immutable.. // // Do not use the deprecated [PutTransactionsParams.Context] with this method: it would be ignored. func (a *Client) PutTransactionsContext(ctx context.Context, params *PutTransactionsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PutTransactionsOK, error) { diff --git a/api/members/members_models/transaction.go b/api/members/members_models/transaction.go index 2590c9a..13a8fa1 100644 --- a/api/members/members_models/transaction.go +++ b/api/members/members_models/transaction.go @@ -8,63 +8,344 @@ package members_models import ( "context" + "encoding/json" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag/jsonutils" + "github.com/go-openapi/swag/typeutils" + "github.com/go-openapi/validate" ) -// Transaction transaction +// Transaction Tenant-owned commercial transaction metadata. PaymentMethodID and processor material are deliberately absent. TenantID, identity, audit fields, amount, currency, and transaction date become server-owned after creation. // -// swagger:model transaction +// swagger:model Transaction type Transaction struct { - // amount - Amount float64 `json:"Amount,omitempty"` - - // course ID - CourseID *string `json:"CourseID,omitempty"` + // Exact non-negative DECIMAL(10,2) text; never represented as float64. + // Pattern: ^(0|[1-9][0-9]{0,7})(\.[0-9]{1,2})?$ + Amount string `json:"Amount,omitempty"` // created by ID - CreatedByID *string `json:"CreatedByID,omitempty"` + // Read Only: true + // Format: uuid + CreatedByID *strfmt.UUID `json:"CreatedByID,omitempty"` // created date - CreatedDate *string `json:"CreatedDate,omitempty"` + // Read Only: true + // Format: date-time + CreatedDate *strfmt.DateTime `json:"CreatedDate,omitempty"` // currency - Currency *string `json:"Currency,omitempty"` - - // enrollment ID - EnrollmentID *string `json:"EnrollmentID,omitempty"` + // Pattern: ^[A-Z]{3}$ + Currency string `json:"Currency,omitempty"` // ID - ID string `json:"ID,omitempty"` + // Read Only: true + // Format: uuid + ID strfmt.UUID `json:"ID,omitempty"` // last modified by ID - LastModifiedByID *string `json:"LastModifiedByID,omitempty"` + // Read Only: true + // Format: uuid + LastModifiedByID *strfmt.UUID `json:"LastModifiedByID,omitempty"` // last modified date - LastModifiedDate *string `json:"LastModifiedDate,omitempty"` - - // payment method ID - PaymentMethodID *string `json:"PaymentMethodID,omitempty"` + // Format: date-time + LastModifiedDate *strfmt.DateTime `json:"LastModifiedDate,omitempty"` // status + // Enum: ["pending","completed","failed","refunded"] Status *string `json:"Status,omitempty"` - // transaction date - TransactionDate *string `json:"TransactionDate,omitempty"` + // tenant ID + // Read Only: true + // Format: uuid + TenantID *strfmt.UUID `json:"TenantID,omitempty"` - // user ID - UserID *string `json:"UserID,omitempty"` + // transaction date + // Format: date-time + TransactionDate *strfmt.DateTime `json:"TransactionDate,omitempty"` } // Validate validates this transaction func (m *Transaction) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAmount(formats); err != nil { + res = append(res, err) + } + + if err := m.validateCreatedByID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateCreatedDate(formats); err != nil { + res = append(res, err) + } + + if err := m.validateCurrency(formats); err != nil { + res = append(res, err) + } + + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateLastModifiedByID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateLastModifiedDate(formats); err != nil { + res = append(res, err) + } + + if err := m.validateStatus(formats); err != nil { + res = append(res, err) + } + + if err := m.validateTenantID(formats); err != nil { + res = append(res, err) + } + + if err := m.validateTransactionDate(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } return nil } -// ContextValidate validates this transaction based on context it is used +func (m *Transaction) validateAmount(formats strfmt.Registry) error { + if typeutils.IsZero(m.Amount) { // not required + return nil + } + + if err := validate.Pattern("Amount", "body", m.Amount, `^(0|[1-9][0-9]{0,7})(\.[0-9]{1,2})?$`); err != nil { + return err + } + + return nil +} + +func (m *Transaction) validateCreatedByID(formats strfmt.Registry) error { + if typeutils.IsZero(m.CreatedByID) { // not required + return nil + } + + if err := validate.FormatOf("CreatedByID", "body", "uuid", m.CreatedByID.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *Transaction) validateCreatedDate(formats strfmt.Registry) error { + if typeutils.IsZero(m.CreatedDate) { // not required + return nil + } + + if err := validate.FormatOf("CreatedDate", "body", "date-time", m.CreatedDate.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *Transaction) validateCurrency(formats strfmt.Registry) error { + if typeutils.IsZero(m.Currency) { // not required + return nil + } + + if err := validate.Pattern("Currency", "body", m.Currency, `^[A-Z]{3}$`); err != nil { + return err + } + + return nil +} + +func (m *Transaction) validateID(formats strfmt.Registry) error { + if typeutils.IsZero(m.ID) { // not required + return nil + } + + if err := validate.FormatOf("ID", "body", "uuid", m.ID.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *Transaction) validateLastModifiedByID(formats strfmt.Registry) error { + if typeutils.IsZero(m.LastModifiedByID) { // not required + return nil + } + + if err := validate.FormatOf("LastModifiedByID", "body", "uuid", m.LastModifiedByID.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *Transaction) validateLastModifiedDate(formats strfmt.Registry) error { + if typeutils.IsZero(m.LastModifiedDate) { // not required + return nil + } + + if err := validate.FormatOf("LastModifiedDate", "body", "date-time", m.LastModifiedDate.String(), formats); err != nil { + return err + } + + return nil +} + +var transactionTypeStatusPropEnum []any + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["pending","completed","failed","refunded"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + transactionTypeStatusPropEnum = append(transactionTypeStatusPropEnum, v) + } +} + +const ( + + // TransactionStatusPending captures enum value "pending" + TransactionStatusPending string = "pending" + + // TransactionStatusCompleted captures enum value "completed" + TransactionStatusCompleted string = "completed" + + // TransactionStatusFailed captures enum value "failed" + TransactionStatusFailed string = "failed" + + // TransactionStatusRefunded captures enum value "refunded" + TransactionStatusRefunded string = "refunded" +) + +// prop value enum +func (m *Transaction) validateStatusEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, transactionTypeStatusPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *Transaction) validateStatus(formats strfmt.Registry) error { + if typeutils.IsZero(m.Status) { // not required + return nil + } + + // value enum + if err := m.validateStatusEnum("Status", "body", *m.Status); err != nil { + return err + } + + return nil +} + +func (m *Transaction) validateTenantID(formats strfmt.Registry) error { + if typeutils.IsZero(m.TenantID) { // not required + return nil + } + + if err := validate.FormatOf("TenantID", "body", "uuid", m.TenantID.String(), formats); err != nil { + return err + } + + return nil +} + +func (m *Transaction) validateTransactionDate(formats strfmt.Registry) error { + if typeutils.IsZero(m.TransactionDate) { // not required + return nil + } + + if err := validate.FormatOf("TransactionDate", "body", "date-time", m.TransactionDate.String(), formats); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this transaction based on the context it is used func (m *Transaction) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateCreatedByID(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateCreatedDate(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateID(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateLastModifiedByID(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateTenantID(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *Transaction) contextValidateCreatedByID(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "CreatedByID", "body", m.CreatedByID); err != nil { + return err + } + + return nil +} + +func (m *Transaction) contextValidateCreatedDate(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "CreatedDate", "body", m.CreatedDate); err != nil { + return err + } + + return nil +} + +func (m *Transaction) contextValidateID(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "ID", "body", m.ID); err != nil { + return err + } + + return nil +} + +func (m *Transaction) contextValidateLastModifiedByID(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "LastModifiedByID", "body", m.LastModifiedByID); err != nil { + return err + } + + return nil +} + +func (m *Transaction) contextValidateTenantID(ctx context.Context, formats strfmt.Registry) error { + + if err := validate.ReadOnly(ctx, "TenantID", "body", m.TenantID); err != nil { + return err + } + return nil } diff --git a/api/members/members_models/transaction_request.go b/api/members/members_models/transaction_request.go index 060b8ec..9c1afeb 100644 --- a/api/members/members_models/transaction_request.go +++ b/api/members/members_models/transaction_request.go @@ -15,14 +15,18 @@ import ( "github.com/go-openapi/strfmt" "github.com/go-openapi/swag/jsonutils" "github.com/go-openapi/swag/typeutils" + "github.com/go-openapi/validate" ) -// TransactionRequest An array of Transaction objects +// TransactionRequest Exactly one governed Transaction object // // swagger:model TransactionRequest type TransactionRequest struct { // data + // Required: true + // Max Items: 1 + // Min Items: 1 Data []*Transaction `json:"Data"` } @@ -41,8 +45,19 @@ func (m *TransactionRequest) Validate(formats strfmt.Registry) error { } func (m *TransactionRequest) validateData(formats strfmt.Registry) error { - if typeutils.IsZero(m.Data) { // not required - return nil + + if err := validate.Required("Data", "body", m.Data); err != nil { + return err + } + + iDataSize := int64(len(m.Data)) + + if err := validate.MinItems("Data", "body", iDataSize, 1); err != nil { + return err + } + + if err := validate.MaxItems("Data", "body", iDataSize, 1); err != nil { + return err } for i := 0; i < len(m.Data); i++ { diff --git a/api/members/members_models/transaction_response.go b/api/members/members_models/transaction_response.go index da52d35..7d190dd 100644 --- a/api/members/members_models/transaction_response.go +++ b/api/members/members_models/transaction_response.go @@ -17,7 +17,7 @@ import ( "github.com/go-openapi/swag/typeutils" ) -// TransactionResponse An array of Transaction objects +// TransactionResponse An array of governed, payment-token-free Transaction objects // // swagger:model TransactionResponse type TransactionResponse struct { diff --git a/swagger/external/members-vernonkeenan.yaml b/swagger/external/members-vernonkeenan.yaml index 3b7f2b9..c96f2e9 100644 --- a/swagger/external/members-vernonkeenan.yaml +++ b/swagger/external/members-vernonkeenan.yaml @@ -162,6 +162,20 @@ parameters: type: string minLength: 1 maxLength: 36 + transactionIdQuery: + description: Record Id of a governed Transaction + in: query + name: transactionId + required: false + type: string + format: uuid + transactionTenantId: + description: Tenant that owns the Transaction. Members verifies the active human membership and never accepts TenantID from the body. + in: query + name: tenantId + required: true + type: string + format: uuid ClusterRequest: description: Exactly one governed Cluster record in: body @@ -4273,10 +4287,11 @@ paths: - TrackEvents /transactions: get: - description: Return a list of Transaction records from the datastore + description: Return tenant-scoped, payment-token-free Transaction records. Requires members:transaction:read and active Owner or Manager access in tenantId. operationId: getTransactions parameters: - - $ref: "#/parameters/idQuery" + - $ref: "#/parameters/transactionIdQuery" + - $ref: "#/parameters/transactionTenantId" - $ref: "#/parameters/limitQuery" - $ref: "#/parameters/offsetQuery" responses: @@ -4294,14 +4309,16 @@ paths: $ref: "#/responses/ServerError" security: - ApiKeyAuth: [] - summary: Get a list of Transactions + kvSessionCookie: [] + summary: Get governed tenant Transactions tags: - Transactions post: - description: Create Transactions + description: Create one tenant-scoped Transaction in pending state. Requires members:transaction:create and active Owner or Manager access. IDs, tenant ownership, lifecycle state, audit fields, and all payment-method references are server-owned. operationId: postTransactions parameters: - $ref: "#/parameters/TransactionRequest" + - $ref: "#/parameters/transactionTenantId" responses: "200": $ref: "#/responses/TransactionResponse" @@ -4317,14 +4334,16 @@ paths: $ref: "#/responses/ServerError" security: - ApiKeyAuth: [] - summary: Create new Transactions + kvSessionCookie: [] + summary: Create a governed Transaction tags: - Transactions put: - description: Update Transaction + description: CAS-transition one tenant-scoped Transaction. Requires members:transaction:update, active Owner or Manager access, and the current LastModifiedDate. Amount, currency, transaction date, tenant ownership, relationships, audit fields, and payment-method references are immutable. operationId: putTransactions parameters: - $ref: "#/parameters/TransactionRequest" + - $ref: "#/parameters/transactionTenantId" responses: "200": $ref: "#/responses/TransactionResponse" @@ -4332,6 +4351,8 @@ paths: $ref: "#/responses/Unauthorized" "403": $ref: "#/responses/AccessForbidden" + "409": + $ref: "#/responses/Conflict" "404": $ref: "#/responses/NotFound" "422": @@ -4340,7 +4361,8 @@ paths: $ref: "#/responses/ServerError" security: - ApiKeyAuth: [] - summary: Update Transaction + kvSessionCookie: [] + summary: Transition a governed Transaction tags: - Transactions /users/onboard: @@ -6123,22 +6145,72 @@ definitions: $ref: "../../lib/swagger/defs/ticket.yaml#/Ticket" type: array type: object + Transaction: + description: Tenant-owned commercial transaction metadata. PaymentMethodID and processor material are deliberately absent. TenantID, identity, audit fields, amount, currency, and transaction date become server-owned after creation. + type: object + properties: + ID: + type: string + format: uuid + readOnly: true + Amount: + type: string + pattern: '^(0|[1-9][0-9]{0,7})(\.[0-9]{1,2})?$' + description: Exact non-negative DECIMAL(10,2) text; never represented as float64. + CreatedByID: + type: string + format: uuid + x-nullable: true + readOnly: true + CreatedDate: + type: string + format: date-time + x-nullable: true + readOnly: true + Currency: + type: string + pattern: '^[A-Z]{3}$' + LastModifiedByID: + type: string + format: uuid + x-nullable: true + readOnly: true + LastModifiedDate: + type: string + format: date-time + x-nullable: true + Status: + type: string + enum: [pending, completed, failed, refunded] + x-nullable: true + TenantID: + type: string + format: uuid + x-nullable: true + readOnly: true + TransactionDate: + type: string + format: date-time + x-nullable: true TransactionRequest: - description: An array of Transaction objects + description: Exactly one governed Transaction object + required: [Data] properties: Data: items: - $ref: "../../lib/swagger/defs/transaction.yaml#/Transaction" + $ref: "#/definitions/Transaction" + minItems: 1 + maxItems: 1 type: array type: object TransactionResponse: - description: An array of Transaction objects + description: An array of governed, payment-token-free Transaction objects properties: Meta: $ref: "#/definitions/ResponseMeta" Data: items: - $ref: "../../lib/swagger/defs/transaction.yaml#/Transaction" + $ref: "#/definitions/Transaction" type: array type: object UserRequest: diff --git a/swagger/members-vernonkeenan.yaml b/swagger/members-vernonkeenan.yaml index 3859645..889e0d5 100644 --- a/swagger/members-vernonkeenan.yaml +++ b/swagger/members-vernonkeenan.yaml @@ -162,6 +162,20 @@ parameters: type: string minLength: 1 maxLength: 36 + transactionIdQuery: + description: Record Id of a governed Transaction + in: query + name: transactionId + required: false + type: string + format: uuid + transactionTenantId: + description: Tenant that owns the Transaction. Members verifies the active human membership and never accepts TenantID from the body. + in: query + name: tenantId + required: true + type: string + format: uuid ClusterRequest: description: Exactly one governed Cluster record in: body @@ -4273,10 +4287,11 @@ paths: - TrackEvents /transactions: get: - description: Return a list of Transaction records from the datastore + description: Return tenant-scoped, payment-token-free Transaction records. Requires members:transaction:read and active Owner or Manager access in tenantId. operationId: getTransactions parameters: - - $ref: "#/parameters/idQuery" + - $ref: "#/parameters/transactionIdQuery" + - $ref: "#/parameters/transactionTenantId" - $ref: "#/parameters/limitQuery" - $ref: "#/parameters/offsetQuery" responses: @@ -4294,14 +4309,16 @@ paths: $ref: "#/responses/ServerError" security: - ApiKeyAuth: [] - summary: Get a list of Transactions + kvSessionCookie: [] + summary: Get governed tenant Transactions tags: - Transactions post: - description: Create Transactions + description: Create one tenant-scoped Transaction in pending state. Requires members:transaction:create and active Owner or Manager access. IDs, tenant ownership, lifecycle state, audit fields, and all payment-method references are server-owned. operationId: postTransactions parameters: - $ref: "#/parameters/TransactionRequest" + - $ref: "#/parameters/transactionTenantId" responses: "200": $ref: "#/responses/TransactionResponse" @@ -4317,14 +4334,16 @@ paths: $ref: "#/responses/ServerError" security: - ApiKeyAuth: [] - summary: Create new Transactions + kvSessionCookie: [] + summary: Create a governed Transaction tags: - Transactions put: - description: Update Transaction + description: CAS-transition one tenant-scoped Transaction. Requires members:transaction:update, active Owner or Manager access, and the current LastModifiedDate. Amount, currency, transaction date, tenant ownership, relationships, audit fields, and payment-method references are immutable. operationId: putTransactions parameters: - $ref: "#/parameters/TransactionRequest" + - $ref: "#/parameters/transactionTenantId" responses: "200": $ref: "#/responses/TransactionResponse" @@ -4332,6 +4351,8 @@ paths: $ref: "#/responses/Unauthorized" "403": $ref: "#/responses/AccessForbidden" + "409": + $ref: "#/responses/Conflict" "404": $ref: "#/responses/NotFound" "422": @@ -4340,7 +4361,8 @@ paths: $ref: "#/responses/ServerError" security: - ApiKeyAuth: [] - summary: Update Transaction + kvSessionCookie: [] + summary: Transition a governed Transaction tags: - Transactions /users/onboard: @@ -6123,22 +6145,72 @@ definitions: $ref: "../../lib/swagger/defs/ticket.yaml#/Ticket" type: array type: object + Transaction: + description: Tenant-owned commercial transaction metadata. PaymentMethodID and processor material are deliberately absent. TenantID, identity, audit fields, amount, currency, and transaction date become server-owned after creation. + type: object + properties: + ID: + type: string + format: uuid + readOnly: true + Amount: + type: string + pattern: '^(0|[1-9][0-9]{0,7})(\.[0-9]{1,2})?$' + description: Exact non-negative DECIMAL(10,2) text; never represented as float64. + CreatedByID: + type: string + format: uuid + x-nullable: true + readOnly: true + CreatedDate: + type: string + format: date-time + x-nullable: true + readOnly: true + Currency: + type: string + pattern: '^[A-Z]{3}$' + LastModifiedByID: + type: string + format: uuid + x-nullable: true + readOnly: true + LastModifiedDate: + type: string + format: date-time + x-nullable: true + Status: + type: string + enum: [pending, completed, failed, refunded] + x-nullable: true + TenantID: + type: string + format: uuid + x-nullable: true + readOnly: true + TransactionDate: + type: string + format: date-time + x-nullable: true TransactionRequest: - description: An array of Transaction objects + description: Exactly one governed Transaction object + required: [Data] properties: Data: items: - $ref: "../../lib/swagger/defs/transaction.yaml#/Transaction" + $ref: "#/definitions/Transaction" + minItems: 1 + maxItems: 1 type: array type: object TransactionResponse: - description: An array of Transaction objects + description: An array of governed, payment-token-free Transaction objects properties: Meta: $ref: "#/definitions/ResponseMeta" Data: items: - $ref: "../../lib/swagger/defs/transaction.yaml#/Transaction" + $ref: "#/definitions/Transaction" type: array type: object UserRequest: