feat(contracts): Add legal export package and ESIGN compliance improvements
- Add export_legal endpoint for signed contracts that generates a ZIP with: - Signed contract PDF - Audit certificate PDF with signature details and hash verification - Machine-readable signature_record.json - Integrity verification report - README documentation - Add audit certificate template with: - Contract and signature information - Consent records with exact legal text - Document integrity verification (SHA-256 hash comparison) - ESIGN Act and UETA compliance statement - Update ContractSigning page for ESIGN/UETA compliance: - Consent checkbox text now matches backend-stored legal text - Added proper legal notice with ESIGN Act references - Add signed_at field to ContractListSerializer - Add view/print buttons for signed contracts in Contracts page - Allow viewing signed contracts via public signing URL 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -205,7 +205,7 @@ export const useContracts = (filters?: {
|
||||
template_version: c.template_version,
|
||||
scope: c.scope as ContractScope,
|
||||
status: c.status,
|
||||
content: c.content,
|
||||
content: c.content_html || c.content, // Backend returns content_html
|
||||
customer: c.customer ? String(c.customer) : undefined,
|
||||
customer_name: c.customer_name || undefined,
|
||||
customer_email: c.customer_email || undefined,
|
||||
@@ -219,7 +219,7 @@ export const useContracts = (filters?: {
|
||||
expires_at: c.expires_at,
|
||||
voided_at: c.voided_at,
|
||||
voided_reason: c.voided_reason,
|
||||
public_token: c.public_token,
|
||||
public_token: c.signing_token || c.public_token, // Backend returns signing_token
|
||||
created_at: c.created_at,
|
||||
updated_at: c.updated_at,
|
||||
}));
|
||||
@@ -258,7 +258,7 @@ export const useContract = (id: string) => {
|
||||
expires_at: data.expires_at,
|
||||
voided_at: data.voided_at,
|
||||
voided_reason: data.voided_reason,
|
||||
public_token: data.public_token,
|
||||
public_token: data.signing_token || data.public_token, // Backend returns signing_token
|
||||
created_at: data.created_at,
|
||||
updated_at: data.updated_at,
|
||||
};
|
||||
@@ -270,9 +270,9 @@ export const useContract = (id: string) => {
|
||||
|
||||
interface ContractInput {
|
||||
template: string;
|
||||
customer?: string;
|
||||
appointment?: string;
|
||||
service?: string;
|
||||
customer_id?: string;
|
||||
event_id?: string;
|
||||
send_email?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -352,8 +352,8 @@ export const usePublicContract = (token: string) => {
|
||||
return useQuery<ContractPublicView>({
|
||||
queryKey: ['public-contracts', token],
|
||||
queryFn: async () => {
|
||||
// Use a plain axios instance without auth
|
||||
const { data } = await apiClient.get(`/contracts/public/${token}/`);
|
||||
// Use the public signing endpoint
|
||||
const { data } = await apiClient.get(`/contracts/sign/${token}/`);
|
||||
return data;
|
||||
},
|
||||
enabled: !!token,
|
||||
@@ -368,21 +368,57 @@ export const useSignContract = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
token,
|
||||
signature_data,
|
||||
signer_name,
|
||||
signer_email,
|
||||
consent_checkbox_checked,
|
||||
electronic_consent_given,
|
||||
}: {
|
||||
token: string;
|
||||
signature_data: string;
|
||||
signer_name: string;
|
||||
signer_email: string;
|
||||
consent_checkbox_checked: boolean;
|
||||
electronic_consent_given: boolean;
|
||||
}) => {
|
||||
const { data } = await apiClient.post(`/contracts/public/${token}/sign/`, {
|
||||
signature_data,
|
||||
const { data } = await apiClient.post(`/contracts/sign/${token}/`, {
|
||||
signer_name,
|
||||
signer_email,
|
||||
consent_checkbox_checked,
|
||||
electronic_consent_given,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to export a legal compliance package for a signed contract
|
||||
*/
|
||||
export const useExportLegalPackage = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
const response = await apiClient.get(`/contracts/${id}/export_legal/`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
// Create download link
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
|
||||
// Extract filename from Content-Disposition header if available
|
||||
const contentDisposition = response.headers['content-disposition'];
|
||||
let filename = `legal_export_${id}.zip`;
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename="?([^";\n]+)"?/);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = filenameMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
link.setAttribute('download', filename);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user