Skip to content
Merged
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
50 changes: 50 additions & 0 deletions exchange/consent-engine/v1/models/consent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package models

import (
"time"

"github.com/google/uuid"
)

// ConsentRecord represents a consent record in the system
type ConsentRecord struct {
// ConsentID is the unique identifier for the consent record
ConsentID uuid.UUID `gorm:"column:consent_id;type:uuid;primaryKey;default:gen_random_uuid()" json:"consent_id"`
// OwnerID is the unique identifier for the data owner
OwnerID string `gorm:"column:owner_id;type:varchar(255);not null;index:idx_consent_records_owner_id" json:"owner_id"`
// OwnerEmail is the email address of the data owner
OwnerEmail string `gorm:"column:owner_email;type:varchar(255);not null;index:idx_consent_records_owner_email" json:"owner_email"`
// AppID is the unique identifier for the consumer application
AppID string `gorm:"column:app_id;type:varchar(255);not null;index:idx_consent_records_app_id" json:"app_id"`
// Status is the status of the consent record: pending, approved, rejected, expired, revoked
Status string `gorm:"column:status;type:varchar(50);not null;index:idx_consent_records_status" json:"status"`
// Type is the type of consent mechanism "realtime" or "offline"
Type string `gorm:"column:type;type:varchar(50);not null" json:"type"`
// CreatedAt is the timestamp when the consent record was created
CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;not null;default:CURRENT_TIMESTAMP;index:idx_consent_records_created_at" json:"created_at"`
// UpdatedAt is the timestamp when the consent record was last updated
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
// PendingExpiresAt is the timestamp when a pending consent expires (timeout waiting for approval/denial)
// Set when status is pending, cleared when status changes to approved/rejected
PendingExpiresAt *time.Time `gorm:"column:pending_expires_at;type:timestamp with time zone;index:idx_consent_records_pending_expires_at" json:"pending_expires_at,omitempty"`
// GrantExpiresAt is the timestamp when an approved consent grant expires
// Calculated by adding GrantDuration to the current time when consent is approved
// Only set when status is approved
GrantExpiresAt *time.Time `gorm:"column:grant_expires_at;type:timestamp with time zone;index:idx_consent_records_grant_expires_at" json:"grant_expires_at,omitempty"`
// GrantDuration is the duration to add to current time when approving consent (e.g., "P30D", "1h")
// Used to calculate GrantExpiresAt: GrantExpiresAt = current_time + GrantDuration
GrantDuration string `gorm:"column:grant_duration;type:varchar(50);not null" json:"grant_duration"`
// Fields is the list of data fields that require consent (stored as array of field names)
Fields []ConsentField `gorm:"column:fields;type:jsonb;not null" json:"fields"`
// SessionID is the session identifier for tracking the consent flow
SessionID string `gorm:"column:session_id;type:varchar(255);not null" json:"session_id"`
// ConsentPortalURL is the URL to redirect to for consent portal
ConsentPortalURL string `gorm:"column:consent_portal_url;type:text" json:"consent_portal_url"`
// UpdatedBy identifies who last updated the consent (audit field)
UpdatedBy *string `gorm:"column:updated_by;type:varchar(255)" json:"updated_by,omitempty"`
}

// TableName specifies the table name for GORM
func (*ConsentRecord) TableName() string {
return "consent_records"
}
59 changes: 59 additions & 0 deletions exchange/consent-engine/v1/models/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package models

// ConsentStatus represents the status of a consent record
type ConsentStatus string

// ConsentStatus constants
const (
StatusPending ConsentStatus = "pending"
StatusApproved ConsentStatus = "approved"
StatusRejected ConsentStatus = "rejected"
StatusExpired ConsentStatus = "expired"
StatusRevoked ConsentStatus = "revoked"
)

// DefaultPendingTimeoutDuration is the default duration for pending consent expiry
// Pending consents will expire after this duration if not approved or rejected
// Format: ISO 8601 duration (e.g., "P1D" for 1 day, "PT24H" for 24 hours)
const DefaultPendingTimeoutDuration = "P1D" // 1 day default

// ConsentErrorMessage represents an error message
type ConsentErrorMessage string

// ConsentErrorMessage constants
const (
ErrConsentNotFound ConsentErrorMessage = "consent record not found"
ErrConsentCreateFailed ConsentErrorMessage = "failed to create consent record"
ErrConsentUpdateFailed ConsentErrorMessage = "failed to update consent record"
ErrConsentRevokeFailed ConsentErrorMessage = "failed to revoke consent record"
ErrConsentGetFailed ConsentErrorMessage = "failed to get consent records"
ErrConsentExpiryFailed ConsentErrorMessage = "failed to check consent expiry"
ErrPortalRequestFailed ConsentErrorMessage = "failed to process consent portal request"
)

// ConsentErrorCode represents an error code
type ConsentErrorCode string

// ConsentErrorCode constants
const (
ErrorCodeConsentNotFound ConsentErrorCode = "CONSENT_NOT_FOUND"
ErrorCodeInternalError ConsentErrorCode = "INTERNAL_ERROR"
ErrorCodeBadRequest ConsentErrorCode = "BAD_REQUEST"
ErrorCodeUnauthorized ConsentErrorCode = "UNAUTHORIZED"
ErrorCodeForbidden ConsentErrorCode = "FORBIDDEN"
)

// ConsentEngineOperation represents the operation
type ConsentEngineOperation string

// ConsentEngineOperation constants
const (
OpCreateConsent ConsentEngineOperation = "create consent"
OpUpdateConsent ConsentEngineOperation = "update consent"
OpRevokeConsent ConsentEngineOperation = "revoke consent"
OpGetConsentStatus ConsentEngineOperation = "get consent status"
OpGetConsentsByOwner ConsentEngineOperation = "get consents by data owner"
OpGetConsentsByConsumer ConsentEngineOperation = "get consents by consumer"
OpCheckConsentExpiry ConsentEngineOperation = "check consent expiry"
OpProcessPortalRequest ConsentEngineOperation = "process consent portal"
)
127 changes: 127 additions & 0 deletions exchange/consent-engine/v1/models/dtos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package models

import (
"strings"
"time"

"github.com/google/uuid"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)

// Owner represents the owner enum (matches PolicyDecisionPoint Owner type)
type Owner string

const (
OwnerCitizen Owner = "citizen"
)

// ConsentRequest defines the structure for creating a consent record
// GrantDuration is optional - nil means not provided and will use default value
type ConsentRequest struct {
AppID string `json:"app_id"`
ConsentRequirements []ConsentRequirement `json:"consent_requirements"`
GrantDuration *string `json:"grant_duration,omitempty"`
}

// ConsentRequirement represents a consent requirement for a specific owner
type ConsentRequirement struct {
Owner string `json:"owner"`
OwnerID string `json:"owner_id"`
Fields []ConsentField `json:"fields"`
}

// ConsentField represents a field that requires consent
// Matches PolicyDecisionResponseFieldRecord structure from PolicyDecisionPoint
type ConsentField struct {
FieldName string `json:"fieldName"`
SchemaID string `json:"schemaId"`
DisplayName *string `json:"displayName,omitempty"`
Description *string `json:"description,omitempty"`
Owner *Owner `json:"owner,omitempty"`
}

// UpdateConsentRequest defines the structure for updating a consent record
// Status is optional (omitempty) to support PATCH operations where status may not be provided
// Optional fields use pointers to distinguish between "not provided" (nil) and "provided as empty" (pointer to empty value)
type UpdateConsentRequest struct {
Status ConsentStatus `json:"status,omitempty"` // Required for PUT, optional for PATCH
UpdatedBy *string `json:"updated_by,omitempty"` // Optional - nil means not provided
GrantDuration *string `json:"grant_duration,omitempty"` // Optional - nil means not provided
Fields *[]ConsentField `json:"fields,omitempty"` // Optional - nil means not provided
Reason *string `json:"reason,omitempty"` // Optional - nil means not provided
}

// ConsentPortalRequest defines the structure for consent portal interactions
// Reason is optional - nil means not provided, pointer allows distinguishing from empty string
type ConsentPortalRequest struct {
ConsentID uuid.UUID `json:"consent_id"`
Action string `json:"action"` // "approve" or "reject"
DataOwner string `json:"data_owner"`
Reason *string `json:"reason,omitempty"`
}

// ConsentResponse represents the simplified response for consent operations
type ConsentResponse struct {
ConsentID uuid.UUID `json:"consent_id"`
Status string `json:"status"`
ConsentPortalURL *string `json:"consent_portal_url,omitempty"` // Only present when status is pending
}

// ConsentPortalView represents the user-facing consent object for the UI.
// Uses rich field information for better UX in the consent portal
type ConsentPortalView struct {
AppDisplayName string `json:"app_display_name"`
CreatedAt time.Time `json:"created_at"`
Fields []ConsentField `json:"fields"` // Rich field information with display names and descriptions
OwnerName string `json:"owner_name"`
OwnerEmail string `json:"owner_email"`
Status string `json:"status"`
Type string `json:"type"`
}

// Legacy structures for backwards compatibility (deprecated)
type DataField struct {
OwnerType string `json:"owner_type,omitempty"`
OwnerID string `json:"owner_id"`
OwnerEmail string `json:"owner_email"`
Fields []string `json:"fields"`
}

// ToConsentResponse converts a ConsentRecord to a simplified ConsentResponse
// Only includes consent_portal_url when status is pending and the URL is not empty
func (cr *ConsentRecord) ToConsentResponse() ConsentResponse {
response := ConsentResponse{
ConsentID: cr.ConsentID,
Status: cr.Status,
}

// Only include consent_portal_url when status is pending and URL is not empty
if cr.Status == string(StatusPending) && cr.ConsentPortalURL != "" {
portalURL := cr.ConsentPortalURL
response.ConsentPortalURL = &portalURL
}

return response
}

// ToConsentPortalView converts an internal ConsentRecord to a user-facing view.
// Returns rich field information including display names and descriptions for better UX
func (cr *ConsentRecord) ToConsentPortalView() *ConsentPortalView {
// Simple mapping for app_id to a human-readable name.
appDisplayName := strings.ReplaceAll(cr.AppID, "-", " ")
appDisplayName = cases.Title(language.English).String(appDisplayName)

ownerName := strings.ReplaceAll(cr.OwnerID, "-", " ")
ownerName = cases.Title(language.English).String(ownerName)

return &ConsentPortalView{
AppDisplayName: appDisplayName,
CreatedAt: cr.CreatedAt,
Fields: cr.Fields, // Now includes DisplayName, Description, and Owner for rich UI rendering
OwnerName: ownerName,
OwnerEmail: cr.OwnerEmail,
Status: cr.Status,
Type: cr.Type,
}
}
Loading