Add electronic signature
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
424
pkg/coredata/electronic_signature.go
Normal file
424
pkg/coredata/electronic_signature.go
Normal file
@@ -0,0 +1,424 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.gearno.de/x/ref"
|
||||
"go.probo.inc/probo/pkg/crypto/hash"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type ElectronicSignature struct {
|
||||
ID gid.GID `db:"id"`
|
||||
TenantID gid.TenantID `db:"tenant_id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
Status ElectronicSignatureStatus `db:"status"`
|
||||
DocumentType ElectronicSignatureDocumentType `db:"document_type"`
|
||||
FileID gid.GID `db:"file_id"`
|
||||
SignerEmail string `db:"signer_email"`
|
||||
ConsentText string `db:"consent_text"`
|
||||
SignerFullName *string `db:"signer_full_name"`
|
||||
SignerIPAddress *string `db:"signer_ip_address"`
|
||||
SignerUserAgent *string `db:"signer_user_agent"`
|
||||
FileHash *string `db:"file_hash"`
|
||||
Seal *string `db:"seal"`
|
||||
SealVersion int `db:"seal_version"`
|
||||
TSAToken []byte `db:"tsa_token"`
|
||||
SignedAt *time.Time `db:"signed_at"`
|
||||
CertificateFileID *gid.GID `db:"certificate_file_id"`
|
||||
CertificateProcessingStartedAt *time.Time `db:"certificate_processing_started_at"`
|
||||
AttemptCount int `db:"attempt_count"`
|
||||
MaxAttempts int `db:"max_attempts"`
|
||||
LastAttemptedAt *time.Time `db:"last_attempted_at"`
|
||||
LastError *string `db:"last_error"`
|
||||
ProcessingStartedAt *time.Time `db:"processing_started_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
func (es *ElectronicSignature) NewEvent(
|
||||
eventType ElectronicSignatureEventType,
|
||||
eventSource ElectronicSignatureEventSource,
|
||||
) ElectronicSignatureEvent {
|
||||
now := time.Now()
|
||||
|
||||
return ElectronicSignatureEvent{
|
||||
ID: gid.New(es.ID.TenantID(), ElectronicSignatureEventEntityType),
|
||||
ElectronicSignatureID: es.ID,
|
||||
EventType: eventType,
|
||||
EventSource: eventSource,
|
||||
ActorEmail: es.SignerEmail,
|
||||
ActorIPAddress: ref.UnrefOrZero(es.SignerIPAddress),
|
||||
ActorUserAgent: ref.UnrefOrZero(es.SignerUserAgent),
|
||||
OccurredAt: now,
|
||||
CreatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
func (es *ElectronicSignature) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO electronic_signatures (
|
||||
id, tenant_id, organization_id, status, document_type, file_id,
|
||||
signer_email, consent_text, seal_version, attempt_count, max_attempts,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
@id, @tenant_id, @organization_id, @status, @document_type, @file_id,
|
||||
@signer_email, @consent_text, @seal_version, @attempt_count, @max_attempts,
|
||||
@created_at, @updated_at
|
||||
)
|
||||
`
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": es.ID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"organization_id": es.OrganizationID,
|
||||
"status": es.Status,
|
||||
"document_type": es.DocumentType,
|
||||
"file_id": es.FileID,
|
||||
"signer_email": es.SignerEmail,
|
||||
"consent_text": es.ConsentText,
|
||||
"seal_version": es.SealVersion,
|
||||
"attempt_count": es.AttemptCount,
|
||||
"max_attempts": es.MaxAttempts,
|
||||
"created_at": es.CreatedAt,
|
||||
"updated_at": es.UpdatedAt,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert electronic signature: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *ElectronicSignature) Update(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE electronic_signatures SET
|
||||
status = @status,
|
||||
signer_full_name = @signer_full_name,
|
||||
signer_ip_address = @signer_ip_address,
|
||||
signer_user_agent = @signer_user_agent,
|
||||
file_hash = @file_hash,
|
||||
seal = @seal,
|
||||
seal_version = @seal_version,
|
||||
tsa_token = @tsa_token,
|
||||
signed_at = @signed_at,
|
||||
certificate_file_id = @certificate_file_id,
|
||||
certificate_processing_started_at = @certificate_processing_started_at,
|
||||
attempt_count = @attempt_count,
|
||||
max_attempts = @max_attempts,
|
||||
last_attempted_at = @last_attempted_at,
|
||||
last_error = @last_error,
|
||||
processing_started_at = @processing_started_at,
|
||||
updated_at = @updated_at
|
||||
WHERE
|
||||
%s
|
||||
AND id = @id
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": es.ID,
|
||||
"status": es.Status,
|
||||
"signer_full_name": es.SignerFullName,
|
||||
"signer_ip_address": es.SignerIPAddress,
|
||||
"signer_user_agent": es.SignerUserAgent,
|
||||
"file_hash": es.FileHash,
|
||||
"seal": es.Seal,
|
||||
"seal_version": es.SealVersion,
|
||||
"tsa_token": es.TSAToken,
|
||||
"signed_at": es.SignedAt,
|
||||
"certificate_file_id": es.CertificateFileID,
|
||||
"certificate_processing_started_at": es.CertificateProcessingStartedAt,
|
||||
"attempt_count": es.AttemptCount,
|
||||
"max_attempts": es.MaxAttempts,
|
||||
"last_attempted_at": es.LastAttemptedAt,
|
||||
"last_error": es.LastError,
|
||||
"processing_started_at": es.ProcessingStartedAt,
|
||||
"updated_at": es.UpdatedAt,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update electronic signature: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *ElectronicSignature) LoadByID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
id gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id, tenant_id, organization_id, status, document_type, file_id,
|
||||
signer_email, consent_text, signer_full_name, signer_ip_address,
|
||||
signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at,
|
||||
certificate_file_id, certificate_processing_started_at,
|
||||
attempt_count, max_attempts, last_attempted_at, last_error,
|
||||
processing_started_at, created_at, updated_at
|
||||
FROM electronic_signatures
|
||||
WHERE %s AND id = @id
|
||||
LIMIT 1
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"id": id}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query electronic signature: %w", err)
|
||||
}
|
||||
|
||||
sig, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ElectronicSignature])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
return fmt.Errorf("cannot collect electronic signature: %w", err)
|
||||
}
|
||||
|
||||
*es = sig
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *ElectronicSignature) LoadByOrgEmailAndDocType(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
orgID gid.GID,
|
||||
email string,
|
||||
docType ElectronicSignatureDocumentType,
|
||||
fileID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id, tenant_id, organization_id, status, document_type, file_id,
|
||||
signer_email, consent_text, signer_full_name, signer_ip_address,
|
||||
signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at,
|
||||
certificate_file_id, certificate_processing_started_at,
|
||||
attempt_count, max_attempts, last_attempted_at, last_error,
|
||||
processing_started_at, created_at, updated_at
|
||||
FROM electronic_signatures
|
||||
WHERE %s
|
||||
AND organization_id = @organization_id
|
||||
AND signer_email = @signer_email
|
||||
AND document_type = @document_type
|
||||
AND file_id = @file_id
|
||||
LIMIT 1
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"organization_id": orgID,
|
||||
"signer_email": email,
|
||||
"document_type": docType,
|
||||
"file_id": fileID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query electronic signature: %w", err)
|
||||
}
|
||||
|
||||
sig, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ElectronicSignature])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
return fmt.Errorf("cannot collect electronic signature: %w", err)
|
||||
}
|
||||
|
||||
*es = sig
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *ElectronicSignature) LoadNextAcceptedForUpdateSkipLocked(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id, tenant_id, organization_id, status, document_type, file_id,
|
||||
signer_email, consent_text, signer_full_name, signer_ip_address,
|
||||
signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at,
|
||||
certificate_file_id, certificate_processing_started_at,
|
||||
attempt_count, max_attempts, last_attempted_at, last_error,
|
||||
processing_started_at, created_at, updated_at
|
||||
FROM electronic_signatures
|
||||
WHERE status = 'ACCEPTED' AND attempt_count < max_attempts
|
||||
ORDER BY updated_at ASC
|
||||
LIMIT 1
|
||||
FOR UPDATE SKIP LOCKED
|
||||
`
|
||||
|
||||
rows, err := conn.Query(ctx, q)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query accepted signatures: %w", err)
|
||||
}
|
||||
|
||||
sig, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ElectronicSignature])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
return fmt.Errorf("cannot collect electronic signature: %w", err)
|
||||
}
|
||||
|
||||
*es = sig
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *ElectronicSignature) LoadNextCompletedWithoutCertificateForUpdate(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id, tenant_id, organization_id, status, document_type, file_id,
|
||||
signer_email, consent_text, signer_full_name, signer_ip_address,
|
||||
signer_user_agent, file_hash, seal, seal_version, tsa_token, signed_at,
|
||||
certificate_file_id, certificate_processing_started_at,
|
||||
attempt_count, max_attempts, last_attempted_at, last_error,
|
||||
processing_started_at, created_at, updated_at
|
||||
FROM electronic_signatures
|
||||
WHERE status = 'COMPLETED'
|
||||
AND certificate_file_id IS NULL
|
||||
AND certificate_processing_started_at IS NULL
|
||||
ORDER BY signed_at ASC
|
||||
LIMIT 1
|
||||
FOR UPDATE SKIP LOCKED
|
||||
`
|
||||
|
||||
rows, err := conn.Query(ctx, q)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query completed signatures: %w", err)
|
||||
}
|
||||
|
||||
sig, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ElectronicSignature])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrResourceNotFound
|
||||
}
|
||||
return fmt.Errorf("cannot collect electronic signature: %w", err)
|
||||
}
|
||||
|
||||
*es = sig
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResetStaleProcessingSignatures(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
staleAfter time.Duration,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE electronic_signatures
|
||||
SET status = 'ACCEPTED', processing_started_at = NULL, updated_at = NOW()
|
||||
WHERE status = 'PROCESSING'
|
||||
AND processing_started_at < NOW() - $1::interval
|
||||
`
|
||||
_, err := conn.Exec(ctx, q, staleAfter.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot reset stale processing signatures: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *ElectronicSignature) ComputeSeal(version int) (string, error) {
|
||||
switch version {
|
||||
case 1:
|
||||
return es.computeSealV1()
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported seal version %d", version)
|
||||
}
|
||||
}
|
||||
|
||||
func (es *ElectronicSignature) computeSealV1() (string, error) {
|
||||
if es.SignedAt == nil {
|
||||
return "", fmt.Errorf("signed_at must not be nil")
|
||||
}
|
||||
|
||||
fields := []string{
|
||||
es.ID.String(),
|
||||
es.OrganizationID.String(),
|
||||
es.DocumentType.String(),
|
||||
es.FileID.String(),
|
||||
ref.UnrefOrZero(es.FileHash),
|
||||
ref.UnrefOrZero(es.SignerFullName),
|
||||
strings.ToLower(es.SignerEmail),
|
||||
ref.UnrefOrZero(es.SignerIPAddress),
|
||||
ref.UnrefOrZero(es.SignerUserAgent),
|
||||
es.ConsentText,
|
||||
es.SignedAt.UTC().Truncate(time.Microsecond).Format(time.RFC3339Nano),
|
||||
}
|
||||
|
||||
for i, f := range fields {
|
||||
if f == "" {
|
||||
return "", fmt.Errorf("seal field %d must not be empty", i)
|
||||
}
|
||||
if strings.Contains(f, "\n") {
|
||||
return "", fmt.Errorf("seal field %d must not contain newline", i)
|
||||
}
|
||||
}
|
||||
|
||||
input := strings.Join(fields, "\n")
|
||||
return hash.SHA256Hex([]byte(input)), nil
|
||||
}
|
||||
|
||||
func ResetStaleCertificateProcessing(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
staleAfter time.Duration,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE electronic_signatures
|
||||
SET certificate_processing_started_at = NULL, updated_at = NOW()
|
||||
WHERE status = 'COMPLETED'
|
||||
AND certificate_file_id IS NULL
|
||||
AND certificate_processing_started_at IS NOT NULL
|
||||
AND certificate_processing_started_at < NOW() - $1::interval
|
||||
`
|
||||
_, err := conn.Exec(ctx, q, staleAfter.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot reset stale certificate processing: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
145
pkg/coredata/electronic_signature_document_type.go
Normal file
145
pkg/coredata/electronic_signature_document_type.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
ElectronicSignatureDocumentType string
|
||||
)
|
||||
|
||||
const (
|
||||
ElectronicSignatureDocumentTypeNDA ElectronicSignatureDocumentType = "NDA"
|
||||
ElectronicSignatureDocumentTypeDPA ElectronicSignatureDocumentType = "DPA"
|
||||
ElectronicSignatureDocumentTypeMSA ElectronicSignatureDocumentType = "MSA"
|
||||
ElectronicSignatureDocumentTypeSOW ElectronicSignatureDocumentType = "SOW"
|
||||
ElectronicSignatureDocumentTypeSLA ElectronicSignatureDocumentType = "SLA"
|
||||
ElectronicSignatureDocumentTypeTOS ElectronicSignatureDocumentType = "TOS"
|
||||
ElectronicSignatureDocumentTypePrivacyPolicy ElectronicSignatureDocumentType = "PRIVACY_POLICY"
|
||||
ElectronicSignatureDocumentTypeOther ElectronicSignatureDocumentType = "OTHER"
|
||||
|
||||
ESignProcessConsentText = "By typing my full name and clicking Accept, I consent to sign this document electronically and agree that my electronic signature has the same legal validity as a handwritten signature."
|
||||
)
|
||||
|
||||
func ElectronicSignatureDocumentTypes() []ElectronicSignatureDocumentType {
|
||||
return []ElectronicSignatureDocumentType{
|
||||
ElectronicSignatureDocumentTypeNDA,
|
||||
ElectronicSignatureDocumentTypeDPA,
|
||||
ElectronicSignatureDocumentTypeMSA,
|
||||
ElectronicSignatureDocumentTypeSOW,
|
||||
ElectronicSignatureDocumentTypeSLA,
|
||||
ElectronicSignatureDocumentTypeTOS,
|
||||
ElectronicSignatureDocumentTypePrivacyPolicy,
|
||||
ElectronicSignatureDocumentTypeOther,
|
||||
}
|
||||
}
|
||||
|
||||
func (dt ElectronicSignatureDocumentType) MarshalText() ([]byte, error) {
|
||||
return []byte(dt.String()), nil
|
||||
}
|
||||
|
||||
func (dt *ElectronicSignatureDocumentType) UnmarshalText(data []byte) error {
|
||||
val := string(data)
|
||||
|
||||
switch val {
|
||||
case ElectronicSignatureDocumentTypeNDA.String():
|
||||
*dt = ElectronicSignatureDocumentTypeNDA
|
||||
case ElectronicSignatureDocumentTypeDPA.String():
|
||||
*dt = ElectronicSignatureDocumentTypeDPA
|
||||
case ElectronicSignatureDocumentTypeMSA.String():
|
||||
*dt = ElectronicSignatureDocumentTypeMSA
|
||||
case ElectronicSignatureDocumentTypeSOW.String():
|
||||
*dt = ElectronicSignatureDocumentTypeSOW
|
||||
case ElectronicSignatureDocumentTypeSLA.String():
|
||||
*dt = ElectronicSignatureDocumentTypeSLA
|
||||
case ElectronicSignatureDocumentTypeTOS.String():
|
||||
*dt = ElectronicSignatureDocumentTypeTOS
|
||||
case ElectronicSignatureDocumentTypePrivacyPolicy.String():
|
||||
*dt = ElectronicSignatureDocumentTypePrivacyPolicy
|
||||
case ElectronicSignatureDocumentTypeOther.String():
|
||||
*dt = ElectronicSignatureDocumentTypeOther
|
||||
default:
|
||||
return fmt.Errorf("invalid ElectronicSignatureDocumentType value: %q", val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dt ElectronicSignatureDocumentType) String() string {
|
||||
return string(dt)
|
||||
}
|
||||
|
||||
func (dt *ElectronicSignatureDocumentType) Scan(value any) error {
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid scan source for ElectronicSignatureDocumentType, expected string got %T", value)
|
||||
}
|
||||
|
||||
return dt.UnmarshalText([]byte(val))
|
||||
}
|
||||
|
||||
func (dt ElectronicSignatureDocumentType) Value() (driver.Value, error) {
|
||||
return dt.String(), nil
|
||||
}
|
||||
|
||||
func (dt ElectronicSignatureDocumentType) DisplayName() string {
|
||||
switch dt {
|
||||
case ElectronicSignatureDocumentTypeNDA:
|
||||
return "Non-Disclosure Agreement"
|
||||
case ElectronicSignatureDocumentTypeDPA:
|
||||
return "Data Processing Agreement"
|
||||
case ElectronicSignatureDocumentTypeMSA:
|
||||
return "Master Service Agreement"
|
||||
case ElectronicSignatureDocumentTypeSOW:
|
||||
return "Statement of Work"
|
||||
case ElectronicSignatureDocumentTypeSLA:
|
||||
return "Service Level Agreement"
|
||||
case ElectronicSignatureDocumentTypeTOS:
|
||||
return "Terms of Service"
|
||||
case ElectronicSignatureDocumentTypePrivacyPolicy:
|
||||
return "Privacy Policy"
|
||||
default:
|
||||
return string(dt)
|
||||
}
|
||||
}
|
||||
|
||||
func (dt ElectronicSignatureDocumentType) ConsentText() (string, error) {
|
||||
var docAgreement string
|
||||
switch dt {
|
||||
case ElectronicSignatureDocumentTypeNDA:
|
||||
docAgreement = "I agree to the terms of this Non-Disclosure Agreement."
|
||||
case ElectronicSignatureDocumentTypeDPA:
|
||||
docAgreement = "I agree to the terms of this Data Processing Agreement."
|
||||
case ElectronicSignatureDocumentTypeMSA:
|
||||
docAgreement = "I agree to the terms of this Master Service Agreement."
|
||||
case ElectronicSignatureDocumentTypeSOW:
|
||||
docAgreement = "I agree to the terms of this Statement of Work."
|
||||
case ElectronicSignatureDocumentTypeSLA:
|
||||
docAgreement = "I agree to the terms of this Service Level Agreement."
|
||||
case ElectronicSignatureDocumentTypeTOS:
|
||||
docAgreement = "I agree to these Terms of Service."
|
||||
case ElectronicSignatureDocumentTypePrivacyPolicy:
|
||||
docAgreement = "I agree to this Privacy Policy."
|
||||
case ElectronicSignatureDocumentTypeOther:
|
||||
return "", fmt.Errorf("document type OTHER requires explicit consent text")
|
||||
default:
|
||||
return "", fmt.Errorf("unknown document type %q", dt)
|
||||
}
|
||||
|
||||
return docAgreement + " " + ESignProcessConsentText, nil
|
||||
}
|
||||
117
pkg/coredata/electronic_signature_event.go
Normal file
117
pkg/coredata/electronic_signature_event.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type (
|
||||
ElectronicSignatureEvent struct {
|
||||
ID gid.GID `db:"id"`
|
||||
TenantID gid.TenantID `db:"tenant_id"`
|
||||
ElectronicSignatureID gid.GID `db:"electronic_signature_id"`
|
||||
EventType ElectronicSignatureEventType `db:"event_type"`
|
||||
EventSource ElectronicSignatureEventSource `db:"event_source"`
|
||||
ActorEmail string `db:"actor_email"`
|
||||
ActorIPAddress string `db:"actor_ip_address"`
|
||||
ActorUserAgent string `db:"actor_user_agent"`
|
||||
OccurredAt time.Time `db:"occurred_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
ElectronicSignatureEvents []*ElectronicSignatureEvent
|
||||
)
|
||||
|
||||
func (e *ElectronicSignatureEvent) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO electronic_signature_events (
|
||||
id, tenant_id, electronic_signature_id, event_type, event_source,
|
||||
actor_email, actor_ip_address, actor_user_agent,
|
||||
occurred_at, created_at
|
||||
) VALUES (
|
||||
@id, @tenant_id, @electronic_signature_id, @event_type, @event_source,
|
||||
@actor_email, @actor_ip_address, @actor_user_agent,
|
||||
@occurred_at, @created_at
|
||||
)
|
||||
`
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": e.ID,
|
||||
"tenant_id": scope.GetTenantID(),
|
||||
"electronic_signature_id": e.ElectronicSignatureID,
|
||||
"event_type": e.EventType,
|
||||
"event_source": e.EventSource,
|
||||
"actor_email": e.ActorEmail,
|
||||
"actor_ip_address": e.ActorIPAddress,
|
||||
"actor_user_agent": e.ActorUserAgent,
|
||||
"occurred_at": e.OccurredAt,
|
||||
"created_at": e.CreatedAt,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert electronic signature event: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *ElectronicSignatureEvents) LoadBySignatureID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
sigID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT
|
||||
id, tenant_id, electronic_signature_id, event_type, event_source,
|
||||
actor_email, actor_ip_address, actor_user_agent,
|
||||
occurred_at, created_at
|
||||
FROM electronic_signature_events
|
||||
WHERE %s AND electronic_signature_id = @electronic_signature_id
|
||||
ORDER BY occurred_at ASC
|
||||
`
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"electronic_signature_id": sigID,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query electronic signature events: %w", err)
|
||||
}
|
||||
|
||||
events, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ElectronicSignatureEvent])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect electronic signature events: %w", err)
|
||||
}
|
||||
|
||||
*es = events
|
||||
|
||||
return nil
|
||||
}
|
||||
65
pkg/coredata/electronic_signature_event_source.go
Normal file
65
pkg/coredata/electronic_signature_event_source.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
ElectronicSignatureEventSource string
|
||||
)
|
||||
|
||||
const (
|
||||
ElectronicSignatureEventSourceClient ElectronicSignatureEventSource = "CLIENT"
|
||||
ElectronicSignatureEventSourceServer ElectronicSignatureEventSource = "SERVER"
|
||||
)
|
||||
|
||||
func (s ElectronicSignatureEventSource) MarshalText() ([]byte, error) {
|
||||
return []byte(s.String()), nil
|
||||
}
|
||||
|
||||
func (s *ElectronicSignatureEventSource) UnmarshalText(data []byte) error {
|
||||
val := string(data)
|
||||
|
||||
switch val {
|
||||
case ElectronicSignatureEventSourceClient.String():
|
||||
*s = ElectronicSignatureEventSourceClient
|
||||
case ElectronicSignatureEventSourceServer.String():
|
||||
*s = ElectronicSignatureEventSourceServer
|
||||
default:
|
||||
return fmt.Errorf("invalid ElectronicSignatureEventSource value: %q", val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s ElectronicSignatureEventSource) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func (s *ElectronicSignatureEventSource) Scan(value any) error {
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid scan source for ElectronicSignatureEventSource, expected string got %T", value)
|
||||
}
|
||||
|
||||
return s.UnmarshalText([]byte(val))
|
||||
}
|
||||
|
||||
func (s ElectronicSignatureEventSource) Value() (driver.Value, error) {
|
||||
return s.String(), nil
|
||||
}
|
||||
86
pkg/coredata/electronic_signature_event_type.go
Normal file
86
pkg/coredata/electronic_signature_event_type.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
ElectronicSignatureEventType string
|
||||
)
|
||||
|
||||
const (
|
||||
ElectronicSignatureEventTypeDocumentViewed ElectronicSignatureEventType = "DOCUMENT_VIEWED"
|
||||
ElectronicSignatureEventTypeConsentGiven ElectronicSignatureEventType = "CONSENT_GIVEN"
|
||||
ElectronicSignatureEventTypeFullNameTyped ElectronicSignatureEventType = "FULL_NAME_TYPED"
|
||||
ElectronicSignatureEventTypeSignatureAccepted ElectronicSignatureEventType = "SIGNATURE_ACCEPTED"
|
||||
ElectronicSignatureEventTypeSignatureCompleted ElectronicSignatureEventType = "SIGNATURE_COMPLETED"
|
||||
ElectronicSignatureEventTypeSealComputed ElectronicSignatureEventType = "SEAL_COMPUTED"
|
||||
ElectronicSignatureEventTypeTimestampRequested ElectronicSignatureEventType = "TIMESTAMP_REQUESTED"
|
||||
ElectronicSignatureEventTypeCertificateGenerated ElectronicSignatureEventType = "CERTIFICATE_GENERATED"
|
||||
ElectronicSignatureEventTypeProcessingError ElectronicSignatureEventType = "PROCESSING_ERROR"
|
||||
)
|
||||
|
||||
func (t ElectronicSignatureEventType) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t *ElectronicSignatureEventType) UnmarshalText(data []byte) error {
|
||||
val := string(data)
|
||||
|
||||
switch val {
|
||||
case ElectronicSignatureEventTypeDocumentViewed.String():
|
||||
*t = ElectronicSignatureEventTypeDocumentViewed
|
||||
case ElectronicSignatureEventTypeConsentGiven.String():
|
||||
*t = ElectronicSignatureEventTypeConsentGiven
|
||||
case ElectronicSignatureEventTypeFullNameTyped.String():
|
||||
*t = ElectronicSignatureEventTypeFullNameTyped
|
||||
case ElectronicSignatureEventTypeSignatureAccepted.String():
|
||||
*t = ElectronicSignatureEventTypeSignatureAccepted
|
||||
case ElectronicSignatureEventTypeSignatureCompleted.String():
|
||||
*t = ElectronicSignatureEventTypeSignatureCompleted
|
||||
case ElectronicSignatureEventTypeSealComputed.String():
|
||||
*t = ElectronicSignatureEventTypeSealComputed
|
||||
case ElectronicSignatureEventTypeTimestampRequested.String():
|
||||
*t = ElectronicSignatureEventTypeTimestampRequested
|
||||
case ElectronicSignatureEventTypeCertificateGenerated.String():
|
||||
*t = ElectronicSignatureEventTypeCertificateGenerated
|
||||
case ElectronicSignatureEventTypeProcessingError.String():
|
||||
*t = ElectronicSignatureEventTypeProcessingError
|
||||
default:
|
||||
return fmt.Errorf("invalid ElectronicSignatureEventType value: %q", val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t ElectronicSignatureEventType) String() string {
|
||||
return string(t)
|
||||
}
|
||||
|
||||
func (t *ElectronicSignatureEventType) Scan(value any) error {
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid scan source for ElectronicSignatureEventType, expected string got %T", value)
|
||||
}
|
||||
|
||||
return t.UnmarshalText([]byte(val))
|
||||
}
|
||||
|
||||
func (t ElectronicSignatureEventType) Value() (driver.Value, error) {
|
||||
return t.String(), nil
|
||||
}
|
||||
74
pkg/coredata/electronic_signature_status.go
Normal file
74
pkg/coredata/electronic_signature_status.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
ElectronicSignatureStatus string
|
||||
)
|
||||
|
||||
const (
|
||||
ElectronicSignatureStatusPending ElectronicSignatureStatus = "PENDING"
|
||||
ElectronicSignatureStatusAccepted ElectronicSignatureStatus = "ACCEPTED"
|
||||
ElectronicSignatureStatusProcessing ElectronicSignatureStatus = "PROCESSING"
|
||||
ElectronicSignatureStatusCompleted ElectronicSignatureStatus = "COMPLETED"
|
||||
ElectronicSignatureStatusFailed ElectronicSignatureStatus = "FAILED"
|
||||
)
|
||||
|
||||
func (s ElectronicSignatureStatus) MarshalText() ([]byte, error) {
|
||||
return []byte(s.String()), nil
|
||||
}
|
||||
|
||||
func (s *ElectronicSignatureStatus) UnmarshalText(data []byte) error {
|
||||
val := string(data)
|
||||
|
||||
switch val {
|
||||
case ElectronicSignatureStatusPending.String():
|
||||
*s = ElectronicSignatureStatusPending
|
||||
case ElectronicSignatureStatusAccepted.String():
|
||||
*s = ElectronicSignatureStatusAccepted
|
||||
case ElectronicSignatureStatusProcessing.String():
|
||||
*s = ElectronicSignatureStatusProcessing
|
||||
case ElectronicSignatureStatusCompleted.String():
|
||||
*s = ElectronicSignatureStatusCompleted
|
||||
case ElectronicSignatureStatusFailed.String():
|
||||
*s = ElectronicSignatureStatusFailed
|
||||
default:
|
||||
return fmt.Errorf("invalid ElectronicSignatureStatus value: %q", val)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s ElectronicSignatureStatus) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func (s *ElectronicSignatureStatus) Scan(value any) error {
|
||||
val, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid scan source for ElectronicSignatureStatus, expected string got %T", value)
|
||||
}
|
||||
|
||||
return s.UnmarshalText([]byte(val))
|
||||
}
|
||||
|
||||
func (s ElectronicSignatureStatus) Value() (driver.Value, error) {
|
||||
return s.String(), nil
|
||||
}
|
||||
105
pkg/coredata/email_attachment.go
Normal file
105
pkg/coredata/email_attachment.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package coredata
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type (
|
||||
EmailAttachment struct {
|
||||
ID gid.GID `db:"id"`
|
||||
EmailID gid.GID `db:"email_id"`
|
||||
FileID gid.GID `db:"file_id"`
|
||||
Filename string `db:"filename"`
|
||||
// TODO (to drop)
|
||||
ContentType string `db:"content_type"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
}
|
||||
|
||||
EmailAttachments []*EmailAttachment
|
||||
)
|
||||
|
||||
func NewEmailAttachment(emailID, fileID gid.GID, filename, contentType string) *EmailAttachment {
|
||||
return &EmailAttachment{
|
||||
ID: gid.New(gid.NilTenant, EmailAttachmentEntityType),
|
||||
EmailID: emailID,
|
||||
FileID: fileID,
|
||||
Filename: filename,
|
||||
ContentType: contentType,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *EmailAttachment) Insert(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
) error {
|
||||
q := `
|
||||
INSERT INTO email_attachments (id, email_id, file_id, filename, content_type, created_at)
|
||||
VALUES (@id, @email_id, @file_id, @filename, @content_type, @created_at)
|
||||
`
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": a.ID,
|
||||
"email_id": a.EmailID,
|
||||
"file_id": a.FileID,
|
||||
"filename": a.Filename,
|
||||
"content_type": a.ContentType,
|
||||
"created_at": a.CreatedAt,
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot insert email attachment: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *EmailAttachments) LoadByEmailID(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
emailID gid.GID,
|
||||
) error {
|
||||
q := `
|
||||
SELECT id, email_id, file_id, filename, content_type, created_at
|
||||
FROM email_attachments
|
||||
WHERE email_id = @email_id
|
||||
ORDER BY created_at ASC
|
||||
`
|
||||
args := pgx.StrictNamedArgs{
|
||||
"email_id": emailID,
|
||||
}
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query email attachments: %w", err)
|
||||
}
|
||||
|
||||
attachments, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[EmailAttachment])
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot collect email attachments: %w", err)
|
||||
}
|
||||
|
||||
*a = attachments
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -82,6 +82,9 @@ const (
|
||||
WebhookSubscriptionEntityType uint16 = 56
|
||||
WebhookDataEntityType uint16 = 57
|
||||
WebhookEventEntityType uint16 = 58
|
||||
ElectronicSignatureEntityType uint16 = 59
|
||||
ElectronicSignatureEventEntityType uint16 = 60
|
||||
EmailAttachmentEntityType uint16 = 61
|
||||
)
|
||||
|
||||
func NewEntityFromID(id gid.GID) (any, bool) {
|
||||
@@ -200,6 +203,12 @@ func NewEntityFromID(id gid.GID) (any, bool) {
|
||||
return &WebhookData{ID: id}, true
|
||||
case WebhookEventEntityType:
|
||||
return &WebhookEvent{ID: id}, true
|
||||
case ElectronicSignatureEntityType:
|
||||
return &ElectronicSignature{ID: id}, true
|
||||
case ElectronicSignatureEventEntityType:
|
||||
return &ElectronicSignatureEvent{ID: id}, true
|
||||
case EmailAttachmentEntityType:
|
||||
return &EmailAttachment{ID: id}, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
|
||||
94
pkg/coredata/migrations/20260213T150000Z.sql
Normal file
94
pkg/coredata/migrations/20260213T150000Z.sql
Normal file
@@ -0,0 +1,94 @@
|
||||
-- Electronic signature enums
|
||||
CREATE TYPE electronic_signature_document_type AS ENUM (
|
||||
'NDA', 'DPA', 'MSA', 'SOW', 'SLA', 'TOS', 'PRIVACY_POLICY', 'OTHER'
|
||||
);
|
||||
|
||||
CREATE TYPE electronic_signature_status AS ENUM (
|
||||
'PENDING',
|
||||
'ACCEPTED',
|
||||
'PROCESSING',
|
||||
'COMPLETED',
|
||||
'FAILED'
|
||||
);
|
||||
|
||||
CREATE TYPE electronic_signature_event_type AS ENUM (
|
||||
'DOCUMENT_VIEWED', 'CONSENT_GIVEN', 'FULL_NAME_TYPED',
|
||||
'SIGNATURE_ACCEPTED',
|
||||
'SIGNATURE_COMPLETED', 'SEAL_COMPUTED', 'TIMESTAMP_REQUESTED',
|
||||
'CERTIFICATE_GENERATED',
|
||||
'PROCESSING_ERROR'
|
||||
);
|
||||
|
||||
CREATE TYPE electronic_signature_event_source AS ENUM ('CLIENT', 'SERVER');
|
||||
|
||||
-- Electronic signatures table
|
||||
CREATE TABLE electronic_signatures (
|
||||
id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
organization_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
status electronic_signature_status NOT NULL DEFAULT 'PENDING',
|
||||
document_type electronic_signature_document_type NOT NULL,
|
||||
file_id TEXT NOT NULL REFERENCES files(id),
|
||||
signer_email CITEXT NOT NULL,
|
||||
|
||||
-- Set at creation time (PENDING):
|
||||
consent_text TEXT NOT NULL,
|
||||
|
||||
-- Set when status transitions to ACCEPTED (signer submits):
|
||||
signer_full_name TEXT,
|
||||
signer_ip_address TEXT,
|
||||
signer_user_agent TEXT,
|
||||
|
||||
-- Set when status transitions to COMPLETED (worker finishes):
|
||||
file_hash TEXT,
|
||||
seal TEXT,
|
||||
seal_version INT NOT NULL DEFAULT 1,
|
||||
tsa_token BYTEA,
|
||||
signed_at TIMESTAMPTZ,
|
||||
|
||||
-- Set by certificate worker after COMPLETED:
|
||||
certificate_file_id TEXT REFERENCES files(id),
|
||||
certificate_processing_started_at TIMESTAMPTZ,
|
||||
|
||||
-- Async processing state:
|
||||
attempt_count INT NOT NULL DEFAULT 0,
|
||||
max_attempts INT NOT NULL DEFAULT 10,
|
||||
last_attempted_at TIMESTAMPTZ,
|
||||
last_error TEXT,
|
||||
processing_started_at TIMESTAMPTZ,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL,
|
||||
|
||||
-- file_id in the constraint allows re-signing when the org replaces the NDA file.
|
||||
UNIQUE(organization_id, signer_email, document_type, file_id)
|
||||
);
|
||||
|
||||
-- Electronic signature events table
|
||||
CREATE TABLE electronic_signature_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
electronic_signature_id TEXT NOT NULL REFERENCES electronic_signatures(id) ON DELETE CASCADE,
|
||||
event_type electronic_signature_event_type NOT NULL,
|
||||
event_source electronic_signature_event_source NOT NULL,
|
||||
actor_email CITEXT NOT NULL,
|
||||
actor_ip_address TEXT NOT NULL,
|
||||
actor_user_agent TEXT NOT NULL,
|
||||
occurred_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_esig_events_signature
|
||||
ON electronic_signature_events (electronic_signature_id, occurred_at ASC);
|
||||
|
||||
-- Email attachments table (generic, not esign-specific)
|
||||
CREATE TABLE email_attachments (
|
||||
id TEXT PRIMARY KEY,
|
||||
email_id TEXT NOT NULL REFERENCES emails(id) ON DELETE CASCADE,
|
||||
file_id TEXT NOT NULL REFERENCES files(id),
|
||||
filename TEXT NOT NULL,
|
||||
content_type TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_email_attachments_email_id ON email_attachments (email_id);
|
||||
Reference in New Issue
Block a user