-
Notifications
You must be signed in to change notification settings - Fork 368
Add textual signature to generated PDFs #1514
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
afb23a6
1ea569e
01bb7bf
37f5f55
157370a
f559862
7e845ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| <style> | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| body { | ||
| font-family: "ABC Whyte", sans-serif; | ||
| } | ||
|
|
||
| .column { | ||
| display: grid; | ||
| gap: 1rem; | ||
| } | ||
|
|
||
| .item { | ||
| display: grid; | ||
| gap: 0.5rem; | ||
| border-bottom: 1px solid #1D1E172E; | ||
| } | ||
|
|
||
| .label { | ||
| text-transform: uppercase; | ||
| color: #1D1E17A1; | ||
| font-weight: 500; | ||
| font-size: 0.75rem; | ||
| } | ||
|
|
||
| .signature { | ||
| font-size: 1.25rem; | ||
| font-style: italic; | ||
| border: solid 1px #1D1E172E; | ||
| padding: 1rem; | ||
| font-family: "Caveat", cursive; | ||
| text-align: center; | ||
| } | ||
| </style> | ||
|
|
||
| <%= body_html %> | ||
| <br /><br /><br /> | ||
| <b>IN WITNESS WHEREOF</b>, the parties have executed this Agreement as of the Effective Date. | ||
| <% | ||
| company_representative = document.company.primary_admin.user | ||
| signature = document.signatures.first | ||
| signatory = signature&.user | ||
| %> | ||
| <div style="display: grid; grid-template-columns: 1fr 1fr; margin-top: 1.25rem; gap: 2rem"> | ||
| <div class="column"> | ||
| <b>CLIENT</b> | ||
| <div class="item" style="border-bottom: none;"> | ||
| <div class="label">Signature</div> | ||
| <div class="signature"> | ||
| <%= company_representative.legal_name %> | ||
| </div> | ||
| </div> | ||
| <div class="item"> | ||
| <div class="label">Name</div> | ||
| <%= company_representative.legal_name %> | ||
| </div> | ||
| <div class="item"> | ||
| <div class="label">Title</div> | ||
| Chief Executive Officer | ||
| </div> | ||
| <div class="item"> | ||
| <div class="label">Country</div> | ||
| <%= company_representative.display_country %> | ||
| </div> | ||
| <div class="item"> | ||
| <div class="label">Address</div> | ||
| <%= "#{company_representative.street_address}, #{company_representative.city}, #{company_representative.state}" %> | ||
| </div> | ||
| <div class="item"> | ||
| <div class="label">Email</div> | ||
| <%= company_representative.display_email %> | ||
| </div> | ||
| <div class="item"> | ||
| <div class="label">Date signed</div> | ||
| <%= company_representative.created_at.strftime("%B %d, %Y") %> | ||
| </div> | ||
| </div> | ||
| <div class="column"> | ||
| <b>CONTRACTOR</b> | ||
| <div class="item" style="border-bottom: none;"> | ||
| <div class="label">Signature</div> | ||
| <div class="signature"> | ||
| <%= signatory&.legal_name %> | ||
| </div> | ||
| </div> | ||
| <div class="item"> | ||
| <div class="label">Name</div> | ||
| <%= signatory&.legal_name %> | ||
| </div> | ||
| <div class="item"> | ||
| <div class="label">Legal Entity</div> | ||
| <%= signatory&.billing_entity_name %> | ||
| </div> | ||
| <div class="item"> | ||
| <div class="label">Country</div> | ||
| <%= signatory&.display_country %> | ||
| </div> | ||
| <div class="item"> | ||
| <div class="label">Address</div> | ||
| <% if signatory.present? %> | ||
| <%= "#{signatory.street_address}, #{signatory.city}, #{signatory.state}" %> | ||
| <% end %> | ||
| </div> | ||
| <div class="item"> | ||
| <div class="label">Email</div> | ||
| <%= signatory&.display_email %> | ||
| </div> | ||
| <div class="item"> | ||
| <div class="label">Date signed</div> | ||
| <%= signature&.signed_at&.strftime("%B %d, %Y") %> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| class AddPrimaryAdminIdToCompanies < ActiveRecord::Migration[8.0] | ||
| def change | ||
| add_reference :companies, :primary_admin, null: true, index: true | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,7 @@ | |
| # | ||
| # It's strongly recommended that you check this file into your version control system. | ||
|
|
||
| ActiveRecord::Schema[8.0].define(version: 2025_10_02_213957) do | ||
| ActiveRecord::Schema[8.0].define(version: 2025_12_25_151433) do | ||
| # These are extensions that must be enabled in order to support this database | ||
| enable_extension "pg_catalog.plpgsql" | ||
|
|
||
|
|
@@ -105,8 +105,10 @@ | |
| t.jsonb "json_data", default: {"flags" => []}, null: false | ||
| t.boolean "equity_enabled", default: false, null: false | ||
| t.string "invite_link" | ||
| t.bigint "primary_admin_id" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to add this to |
||
| t.index ["external_id"], name: "index_companies_on_external_id", unique: true | ||
| t.index ["invite_link"], name: "index_companies_on_invite_link", unique: true | ||
| t.index ["primary_admin_id"], name: "index_companies_on_primary_admin_id" | ||
| end | ||
|
|
||
| create_table "company_administrators", force: :cascade do |t| | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| FactoryBot.define do | ||
| factory :document_signature do | ||
| document | ||
| user | ||
| title { "Signer" } | ||
|
|
||
| trait :signed do | ||
| signed_at { Time.current } | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,7 +89,7 @@ test.describe("Documents", () => { | |
| const { company, adminUser } = await companiesFactory.createCompletedOnboarding(); | ||
| await documentsFactory.create({ companyId: company.id, text: "Test document text" }); | ||
| const { user: recipient } = await usersFactory.create({ legalName: "Recipient 1" }); | ||
| await companyContractorsFactory.create({ companyId: company.id, userId: recipient.id }); | ||
| await companyContractorsFactory.create({ companyId: company.id, userId: recipient.id }, { withoutContract: true }); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes the below cleaner as otherwise the user already has a contract. |
||
| await login(page, adminUser, "/documents"); | ||
| await logout(page); | ||
| await expect(page.getByRole("heading", { name: "Documents" })).toBeVisible(); | ||
|
|
@@ -110,6 +110,16 @@ test.describe("Documents", () => { | |
| await expect(page.getByText("Some other text")).toBeVisible(); | ||
| await page.getByRole("button", { name: "Add your signature" }).click(); | ||
| await page.getByRole("button", { name: "Agree & Submit" }).click(); | ||
|
|
||
| await expect(page.getByText("No results.")).toBeVisible(); | ||
| await page.locator("main").getByRole("button", { name: "Filter" }).click(); | ||
| await page.getByRole("menuitem", { name: "Status" }).click(); | ||
| await page.getByRole("menuitemcheckbox", { name: "All" }).click(); | ||
| await expect(page.getByRole("menuitem")).toHaveCount(0); | ||
| await expect(page.locator("tbody tr")).toHaveCount(1); | ||
| await expect(page.getByText("Signed")).toBeVisible(); | ||
| await page.getByRole("button", { name: "Open menu" }).click(); | ||
| await expect(page.getByRole("menuitem", { name: "Download" })).toBeVisible(); | ||
| }); | ||
|
|
||
| test("shows the correct names for documents", async ({ page }) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ import { | |
| import { companyProcedure, createRouter } from "@/trpc"; | ||
| import { simpleUser } from "@/trpc/routes/users"; | ||
| import { assertDefined } from "@/utils/assert"; | ||
| import { signed_company_document_url } from "@/utils/routes"; | ||
|
|
||
| const visibleDocuments = (companyId: bigint, userId: bigint | SQLWrapper | undefined) => | ||
| and( | ||
|
|
@@ -57,7 +58,7 @@ export const documentsRouter = createRouter({ | |
| ) | ||
| .leftJoin(activeStorageBlobs, eq(activeStorageAttachments.blobId, activeStorageBlobs.id)) | ||
| .where(where) | ||
| .orderBy(desc(documents.id)); | ||
| .orderBy(desc(documents.id), desc(activeStorageAttachments.id)); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To match |
||
|
|
||
| const signatories = await db.query.documentSignatures.findMany({ | ||
| columns: { documentId: true, title: true, signedAt: true }, | ||
|
|
@@ -114,7 +115,7 @@ export const documentsRouter = createRouter({ | |
| .limit(1); | ||
| if (!document) throw new TRPCError({ code: "NOT_FOUND" }); | ||
|
|
||
| return await db.transaction(async (tx) => { | ||
| const result = await db.transaction(async (tx) => { | ||
| await tx | ||
| .update(documentSignatures) | ||
| .set({ signedAt: new Date() }) | ||
|
|
@@ -142,5 +143,16 @@ export const documentsRouter = createRouter({ | |
|
|
||
| return { documentId: input.id, complete: allSigned }; | ||
| }); | ||
|
|
||
| // Generate PDF outside the transaction so signedAt is committed | ||
| if (result.complete) { | ||
| const response = await fetch(signed_company_document_url(ctx.company.externalId, document.documents.id), { | ||
| method: "POST", | ||
| headers: ctx.headers, | ||
| }); | ||
| if (!response.ok) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: await response.text() }); | ||
| } | ||
|
|
||
| return result; | ||
| }), | ||
| }); | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TRPC calls this endpoint to generate a PDF for the signed document.