Chain of custody and provenance tracking plugin for archival records, museum objects, and library materials. Provides comprehensive ownership history documentation with support for certainty levels, Nazi-era provenance checking, cultural property status, and visual timeline visualization.
-- Provenance Agent (who owned/held the item)CREATETABLEIFNOTEXISTSprovenance_agent(idINTAUTO_INCREMENTPRIMARYKEY,actor_idINTNULLCOMMENT'Link to AtoM actor if exists',agent_typeENUM('person','organization','family','unknown')DEFAULT'person',nameVARCHAR(500)NOTNULL,contact_infoTEXTNULL,locationVARCHAR(500)NULL,country_codeVARCHAR(3)NULL,verifiedTINYINT(1)DEFAULT0,verified_byINTNULL,verified_atTIMESTAMPNULL,notesTEXTNULL,created_byINTNULL,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP,updated_atTIMESTAMPDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,INDEXidx_actor(actor_id),INDEXidx_agent_type(agent_type),INDEXidx_name(name(100)))ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ci;-- Provenance Agent i18nCREATETABLEIFNOTEXISTSprovenance_agent_i18n(idINTNOTNULL,cultureVARCHAR(16)NOTNULLDEFAULT'en',nameVARCHAR(500)NULL,biographical_noteTEXTNULL,notesTEXTNULL,PRIMARYKEY(id,culture),FOREIGNKEY(id)REFERENCESprovenance_agent(id)ONDELETECASCADE)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ci;-- Main Provenance RecordCREATETABLEIFNOTEXISTSprovenance_record(idINTAUTO_INCREMENTPRIMARYKEY,information_object_idINTNOTNULL,provenance_agent_idINTNULL,donor_idINTNULL,donor_agreement_idINTNULL,current_statusENUM('owned','on_loan','deposited','unknown','disputed')DEFAULT'owned',custody_typeENUM('permanent','temporary','loan','deposit')DEFAULT'permanent',acquisition_typeENUM('donation','purchase','bequest','transfer','loan','deposit','exchange','field_collection','unknown')DEFAULT'unknown',acquisition_dateDATENULL,acquisition_date_textVARCHAR(255)NULL,acquisition_priceDECIMAL(15,2)NULL,acquisition_currencyVARCHAR(3)NULL,certainty_levelENUM('certain','probable','possible','uncertain','unknown')DEFAULT'unknown',has_gapsTINYINT(1)DEFAULT0,gap_descriptionTEXTNULL,research_statusENUM('not_started','in_progress','complete','inconclusive')DEFAULT'not_started',research_notesTEXTNULL,nazi_era_provenance_checkedTINYINT(1)DEFAULT0,nazi_era_provenance_clearTINYINT(1)NULL,nazi_era_notesTEXTNULL,cultural_property_statusENUM('none','claimed','disputed','repatriated','cleared')DEFAULT'none',cultural_property_notesTEXTNULL,provenance_summaryTEXTNULL,is_completeTINYINT(1)DEFAULT0,is_publicTINYINT(1)DEFAULT1,created_byINTNULL,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP,updated_atTIMESTAMPDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,INDEXidx_info_object(information_object_id),INDEXidx_agent(provenance_agent_id),INDEXidx_donor(donor_id),INDEXidx_status(current_status),INDEXidx_acquisition_type(acquisition_type),INDEXidx_certainty(certainty_level),INDEXidx_nazi_era(nazi_era_provenance_checked,nazi_era_provenance_clear),FOREIGNKEY(provenance_agent_id)REFERENCESprovenance_agent(id)ONDELETESETNULL,CONSTRAINTfk_provenance_record_info_objectFOREIGNKEY(information_object_id)REFERENCESinformation_object(id)ONDELETECASCADE)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ci;-- Provenance EventCREATETABLEIFNOTEXISTSprovenance_event(idINTAUTO_INCREMENTPRIMARYKEY,provenance_record_idINTNOTNULL,from_agent_idINTNULL,to_agent_idINTNULL,event_typeENUM('creation','commission','sale','purchase','auction','gift','donation','bequest','inheritance','descent','loan_out','loan_return','deposit','withdrawal','transfer','exchange','theft','recovery','confiscation','restitution','repatriation','discovery','excavation','import','export','authentication','appraisal','conservation','restoration','accessioning','deaccessioning','unknown','other')NOTNULLDEFAULT'unknown',event_dateDATENULL,event_date_startDATENULL,event_date_endDATENULL,event_date_textVARCHAR(255)NULL,date_certaintyENUM('exact','approximate','estimated','unknown')DEFAULT'unknown',event_locationVARCHAR(500)NULL,event_cityVARCHAR(255)NULL,event_countryVARCHAR(3)NULL,priceDECIMAL(15,2)NULL,currencyVARCHAR(3)NULL,sale_referenceVARCHAR(255)NULL,evidence_typeENUM('documentary','physical','oral','circumstantial','none')DEFAULT'none',evidence_descriptionTEXTNULL,source_referenceTEXTNULL,certaintyENUM('certain','probable','possible','uncertain')DEFAULT'uncertain',sequence_numberINTDEFAULT0,notesTEXTNULL,is_publicTINYINT(1)DEFAULT1,created_byINTNULL,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP,updated_atTIMESTAMPDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,INDEXidx_record(provenance_record_id),INDEXidx_from_agent(from_agent_id),INDEXidx_to_agent(to_agent_id),INDEXidx_event_type(event_type),INDEXidx_event_date(event_date),INDEXidx_sequence(provenance_record_id,sequence_number),FOREIGNKEY(provenance_record_id)REFERENCESprovenance_record(id)ONDELETECASCADE,FOREIGNKEY(from_agent_id)REFERENCESprovenance_agent(id)ONDELETESETNULL,FOREIGNKEY(to_agent_id)REFERENCESprovenance_agent(id)ONDELETESETNULL)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ci;-- Provenance DocumentCREATETABLEIFNOTEXISTSprovenance_document(idINTAUTO_INCREMENTPRIMARYKEY,provenance_record_idINTNULL,provenance_event_idINTNULL,document_typeENUM('deed_of_gift','bill_of_sale','invoice','receipt','auction_catalog','exhibition_catalog','inventory','insurance_record','photograph','correspondence','certificate','customs_document','export_license','import_permit','appraisal','condition_report','newspaper_clipping','publication','oral_history','affidavit','legal_document','other')NOTNULLDEFAULT'other',titleVARCHAR(500)NULL,descriptionTEXTNULL,document_dateDATENULL,document_date_textVARCHAR(255)NULL,filenameVARCHAR(500)NULL,original_filenameVARCHAR(500)NULL,file_pathVARCHAR(1000)NULL,mime_typeVARCHAR(100)NULL,file_sizeINTNULL,external_urlVARCHAR(1000)NULL,archive_referenceVARCHAR(500)NULL,is_publicTINYINT(1)DEFAULT0,created_byINTNULL,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP,updated_atTIMESTAMPDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,INDEXidx_record(provenance_record_id),INDEXidx_event(provenance_event_id),INDEXidx_doc_type(document_type),FOREIGNKEY(provenance_record_id)REFERENCESprovenance_record(id)ONDELETECASCADE,FOREIGNKEY(provenance_event_id)REFERENCESprovenance_event(id)ONDELETECASCADE)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ci;
namespace AhgProvenancePlugin\Service;class ProvenanceService{ /** * Get complete provenance data for an information object * @return array{exists: bool, record: ?object, events: array, documents: array, timeline: array, summary: string} */ public function getProvenanceForObject(int $objectId, string $culture = 'en'): array; /** * Build a timeline from events * @return array Timeline items with date, type, from/to agents, location */ public function buildTimeline(array $events): array; /** * Generate human-readable provenance summary from events */ public function generateSummary(?object $record, array $events): string; /** * Get event type label */ public function getEventTypeLabel(string $type): string; /** * Get all event types organized by category (for dropdowns) */ public function getEventTypes(): array; /** * Get acquisition types (for dropdowns) */ public function getAcquisitionTypes(): array; /** * Get certainty levels (for dropdowns) */ public function getCertaintyLevels(): array; /** * Create or update provenance record * @return int Record ID */ public function createRecord(int $objectId, array $data, string $culture = 'en'): int; /** * Add event to provenance chain * @return int Event ID */ public function addEvent(int $recordId, array $data, string $culture = 'en'): int; /** * Get statistics for dashboard */ public function getStatistics(): array; /** * Search agents by name */ public function searchAgents(string $term): array; /** * Find or create agent * @return int Agent ID */ public function findOrCreateAgent(string $name, string $type = 'person', ?int $actorId = null): int;}
namespace AhgProvenancePlugin\Repository;use Illuminate\Database\Capsule\Manager as DB;class ProvenanceRepository{ /** * Get provenance record for an information object */ public function getByInformationObjectId(int $objectId, string $culture = 'en'): ?object; /** * Get all provenance events for a record (chain of custody) */ public function getEvents(int $recordId, string $culture = 'en'): array; /** * Get documents for a provenance record or event */ public function getDocuments(int $recordId = null, int $eventId = null): array; /** * Get all agents (for dropdowns) */ public function getAllAgents(string $culture = 'en'): array; /** * Search agents by name */ public function searchAgents(string $term, int $limit = 20): array; /** * Create or update provenance record */ public function saveRecord(array $data): int; /** * Save provenance record i18n */ public function saveRecordI18n(int $id, string $culture, array $data): void; /** * Create or update event */ public function saveEvent(array $data): int; /** * Create or update agent */ public function saveAgent(array $data): int; /** * Delete event */ public function deleteEvent(int $eventId): bool; /** * Get records with incomplete provenance */ public function getIncompleteRecords(int $limit = 50): array; /** * Get records needing Nazi-era provenance check */ public function getNaziEraUnchecked(int $limit = 50): array; /** * Get provenance statistics */ public function getStatistics(): array;}
Agent autocomplete functionality:
- Initializes autocomplete on .agent-autocomplete inputs
- Debounced search (300ms delay)
- Minimum 2 characters to trigger search
- Shows dropdown with agent name and type
{"name":"Provenance Tracking","machine_name":"ahgProvenancePlugin","version":"1.0.3","description":"Chain of custody and provenance tracking for archival records, museum objects, and library materials","author":"The Archive and Heritage Group","license":"GPL-3.0","requires":{"atom_framework":">=1.0.0","atom":">=2.8","php":">=8.1"},"dependencies":["ahgCorePlugin"],"optional":{"extensions":["ahgDonorAgreementPlugin","ahgRightsPlugin"]},"tables":["provenance_record","provenance_record_i18n","provenance_event","provenance_event_i18n","provenance_agent","provenance_agent_i18n","provenance_document"],"category":"provenance","load_order":45}
1. User submits edit form
|
v
2. provenanceActions::executeEdit()
|
v
3. processForm()
- Extract form data
- Find/create current agent
- Create/update provenance_record
- Save i18n data
|
v
4. processEvents()
- Delete existing events
- Create new events from form arrays
- Find/create from/to agents
|
v
5. processDocuments()
- Handle file uploads
- Create provenance_document records
|
v
6. Redirect to view page
1. ProvenanceService::getProvenanceForObject()
|
v
2. Repository::getEvents()
- Join with agents
- Order by sequence_number, event_date
|
v
3. ProvenanceService::buildTimeline()
- Format each event for display
- Include date, type, from/to, location, certainty
|
v
4. ProvenanceService::generateSummary()
- If manual summary exists, use it
- Otherwise generate from events