# Project Documents and Signatures
## Requirements
1. Documents must be digitally signed;
2. There should be a completeness mechanism that can be used to determine when a document is fully signed, e.g. how many signatures are required and from whom and how many signatures have been collected so far and from whom. This is to support async signing flows;
3. Once the signed PDF document is generated the actual user signature images should be removed from the PDF (compliance?);
4.
## `DocumentType`
Provides the mean to configure a given document type behavior, providing answers to the following questions:
- Who is supposed to sign a document of this type (via its relationship with `SignaturePosition#signatory`)? e.g.:
- `remodeling_consultant`;
- `signatory_1`;
- `signatory_2`;
- `applicant_1`;
- `applicant_2`;
- `signatory_any`: I believe this may indicate that ANY signatory can fullfil the signature on cases where only one signatory is required to sign;
- `applicant_any`: I believe this may indicate that ANY loan applicant can fullfil the signature on cases where only one applicant is required to sign;
- How many signatures are required in order to completely sign the document? There are flags on the document type that can be used to determine if one or more signatures are required, like `requires_only_one_homeowner_signature`, however, it may be more interesting to rely on the signatory `*_any` to make that more configurable and decoupled from homeowners;
- What data is required for the document (via its relationship with `SignaturePosition#signature_type`)? e.g.:
- `signature`;
- `date`;
- `initials`;
- `confirmation_text`;
## `SignaturePosition`
Allow configuring which data annotation should be collected and applied to a given document type instance. A document type can have many `SignaturePosition`, and we can actually abstract this as being an `AnnotationPosition` since it can represent things other than signatures. Sach annotation position, has information about:
- The type of the annotation, via `SignaturePosition#signature_type`:
- `signature`;
- `date`;
- `initials`;
- `confirmation_text`;
- The `signatory` type whose the annotation belongs to, via `SignaturePosition#signatory`, and can have the following values:
- `remodeling_consultant`;
- `signatory_1`;
- `signatory_2`;
- `applicant_1`;
- `applicant_2`;
- `signatory_any`;
- `applicant_any`;
- The x, y coordinates on the document where the annotation should be placed, via `SignaturePosition#x_pos` and `SignaturePosition#y_pos`;
- The page of the document where the annotation should be placed, via `SignaturePosition#page`;
- The width and height of the document section where the annotation will be placed, via `SignaturePosition#width` and `SignaturePosition#height`;
## Signing a document
Signing a pdf document in Nitro in very simple terms is the simple act of merging an existing PDF document that was generated by Nitro via the `DocumentType` settings, with another PDF document holding nothing but the required image signatures positioned on the correct coordinates and sized accordingly based on the document's `DocumentType` settings.
There's already a fairly reusable [`Documents::SignDocumentService`](https://github.com/powerhome/nitro-web/blob/master/components/documents/app/services/documents/sign_document_service.rb#L7) in Nitro we may be able to leverage in order to centralize this siging process and make the process more reusable.
## Spike
Sketch the algorithm and building blocks needed to determine which document annotations (`SignaturePosition`) are required on a given document:
```rb=
sig_positions = project_document.document_type.signature_positions
people = project_document.people_required_to_sign
pulse_users = Pulse::User.find_by(person: people)
```
## Entity Relation
```ts=
type Signatory = "signatory_1" | "signatory_2" | "applicant_1" | "applicant_2" | "signatory_any" | "applicant_any"
type SignatureType = "signature" | "initials" | "date" | "completion_text"
```
```mermaid
classDiagram
Document <|-- ProjectDocument
Document <|-- StreamDocument
DocumentType --> SignaturePosition : has_many
Document --> DocumentSignature : has_many
DocumentSignature --> Document : belongs_to
DocumentSignature --> SignaturePosition : belongs_to
class DocumentType {
signatory: Signatory
signature_type: SignatureType
}
class SignaturePosition {
}
class Document {
}
class StreamDocument {
}
class ProjectDocument {
}
class DocumentSignature {
}
```
## NitroPdf
Nitro component that provides PDF manipulation capabilities, such as combining pdfs, adding annotations, etc...
```rb=
module NitroPdf
class Annotation
attr_reader :x, :y, :width, :height, :page, :content
def initialize(x:, y:, width:, height:, page:, content:)
@x = x
@y = y
@width = width
@height = height
@page = page
@content = content
end
def with_font(font_name)
previous_font_name = font
font font_name
yield
font previous_font_name
end
def annotate(pdf)
raise NotImplemented, "A subclass must implement this behavior"
end
end
class TextAnnotation < Annotation
attr_reader :font
def initialize(x:, y:, width:, height:, page:, content:, font:)
super
@font = font
end
def annotate(combiner)
temp_signature_pdf = ::Prawn::Document.new(margin: [0, 0, 0, 0], page_size: "LETTER")
with_font(font) do
temp_signature_pdf.draw_text content, at: [50,50],
width: width,
height: height,
size: 16
end
temp_combine_signature_pdf = CombinePDF.parse(temp_signature_pdf.render)
combiner.pages[page - 1] << temp_combine_signature_pdf.pages[0]
end
end
class ImageAnnotation < Annotation
attr_reader :content
def annotate(combiner)
temp_signature_pdf = ::Prawn::Document.new(margin: [0, 0, 0, 0], page_size: "LETTER")
temp_signature_pdf.image(content, at: [x, y],
width: width,
height: height)
temp_combine_signature_pdf = CombinePDF.parse(temp_signature_pdf.render)
combiner.pages[page - 1] << temp_combine_signature_pdf.pages[0]
end
end
def self.annotate(file:, annotations:)
combiner = CombinePDF.parse(file)
annotations.each do |annotation|
annotation.annotate(combiner)
end
combiner.to_pdf
end
end
signature = NitroPdf::ImageAnnotation.new(
content: signature_string_io_image,
x: 123,
y: 321,
width: 200,
height: 100,
page: 2
)
initials = NitroPdf::ImageAnnotation.new(
content: initials_string_io_image,
x: 321,
y: 321,
width: 60,
height: 100,
page: 1
)
# Applying annotations to a document (where in this case annotations are signature images)
#
annotated_pdf = NitroPdf.annotate!(
pdf: project_document.doc,
annotations: [signature, initials]
)
# Combining multiple pdfs
#
combined_pdf = NitroPdf.combine(pdf1, pdf2, pdf3)
# Hiding sections of a pdf
#
geometry = NitroPdf::GeometryAnnotation.new(
x: 123,
y: 321,
width: 123,
height: 321,
page: 2,
color: "#fff"
)
annotated_pdf = NitroPdf.annotate!(
pdf: project_document.doc,
annotations: [geometry]
)
```
## Doc Sign Completeness
1. Completeness logic that determines whether a doc has been partially, fully signed or not signed at all.
1. Generating the final signed doc