# 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