@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix gs1: <https://ref.gs1.org/voc/> .
@prefix schema: <https://schema.org/> .
@prefix oec: <https://ref.openepcis.io/extensions/common/core/> .
@prefix dpp-sh: <https://ref.openepcis.io/extensions/common/core/shapes/> .
@prefix dcterms: <http://purl.org/dc/terms/> .

# =============================================================================
# SHACL Shapes for OpenEPCIS DPP Core Vocabulary
# The version lives in owl:versionInfo on the sh:ShapesGraph node below, which
# scripts/release.ts stamps — a hand-maintained comment here went stale at 0.9.6.
# =============================================================================

<https://ref.openepcis.io/extensions/common/core/shapes/>
    a sh:ShapesGraph ;
    dcterms:title "OpenEPCIS DPP Core SHACL Shapes"@en ;
    dcterms:description "SHACL validation shapes for OpenEPCIS DPP Core vocabulary. Validates value ranges, required properties, and cardinality constraints."@en ;
    owl:versionInfo "0.9.9" ;
    dcterms:created "2025-02-02"^^xsd:date ;
    dcterms:modified "2026-08-10"^^xsd:date ;
    sh:declare [ sh:prefix "dpp" ; sh:namespace "https://ref.openepcis.io/extensions/common/core/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "oec" ; sh:namespace "https://ref.openepcis.io/extensions/common/core/"^^xsd:anyURI ] ;
    sh:declare [ sh:prefix "gs1" ; sh:namespace "https://ref.gs1.org/voc/"^^xsd:anyURI ] .

# =============================================================================
# Economic-operator role obligation
# =============================================================================

# An ESPR Art. 77 role is REQUIRED only where the party actually is an economic
# operator, which is decided by how the node is reached — not by its class.
#
# oec:OperatorInformation is deliberately reused for every party record, and most
# of those parties have no Art. 77 role at all: oec:hasBackupCopyHost is a DPP
# service provider under Art. 10(4), eudr:hasExemptionAuthority is a competent
# authority, eucpr:hasNotifiedBody and eucpr:hasTechnicalAssessmentBody are
# conformity-assessment bodies. A blanket sh:minCount 1 on the class demanded a
# value the seven-role code list cannot supply for any of them.
#
# Targeting the objects of the economic-operator properties instead states the
# obligation exactly where it holds. Module properties reach it through
# rdfs:subPropertyOf (eubat:hasOperatorInformation), so the core shapes need no
# knowledge of the module namespaces — the layering stays intact.
dpp-sh:EconomicOperatorRoleRequired
    a sh:NodeShape ;
    rdfs:label "Economic operator role required"@en ;
    sh:targetObjectsOf oec:hasOperatorInformation ;
    sh:targetObjectsOf oec:hasPassportIssuer ;
    sh:property [
        sh:path oec:hasOperatorRole ;
        sh:minCount 1 ;
        sh:message "An economic operator must declare its ESPR Art. 77 role (oec:hasOperatorRole)."@en
    ] .

# =============================================================================
# Reusable value shapes
# =============================================================================

# Human-readable text that may carry a language tag.
#
# ESPR requires consumer-facing information in the language of the member state
# where the product is placed on the market, so any such value can arrive either
# as a plain string or as one or more language-tagged strings. A language-tagged
# literal has datatype rdf:langString and NEVER xsd:string, so a bare
# `sh:datatype xsd:string` rejects exactly the multilingual case the regulation
# demands. Reference this shape with `sh:node dpp-sh:TranslatableText` instead of
# repeating the alternation, and do not pair it with `sh:maxCount 1` — n
# translations are n values.
dpp-sh:TranslatableText
    a sh:NodeShape ;
    rdfs:label "Translatable text"@en ;
    rdfs:comment "A plain or language-tagged string; the multilingual form ESPR consumer information requires."@en ;
    sh:or (
        [ sh:datatype xsd:string ]
        [ sh:datatype rdf:langString ]
    ) ;
    sh:message "must be text: either a plain string or a language-tagged string"@en .

# =============================================================================
# CircularityPerformance Shape
# =============================================================================

dpp-sh:CircularityPerformanceShape
    a sh:NodeShape ;
    sh:targetClass oec:CircularityPerformance ;
    sh:name "Circularity Performance Shape"@en ;
    sh:description "Validates CircularityPerformance instances"@en ;

    sh:property [
        sh:path oec:recyclableContent ;
        sh:name "recyclableContent"@en ;
        sh:description "Fraction of product that can be recycled (0-1)"@en ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:maxInclusive 1 ;
        sh:maxCount 1 ;
        sh:message "recyclableContent must be a decimal between 0 and 1"@en
    ] ;

    sh:property [
        sh:path oec:utilityFactor ;
        sh:name "utilityFactor"@en ;
        sh:description "Durability relative to industry average (typically 0.5-2.0)"@en ;
        sh:datatype xsd:decimal ;
        sh:minExclusive 0 ;
        sh:maxCount 1 ;
        sh:message "utilityFactor must be a positive decimal"@en
    ] ;

    sh:property [
        sh:path oec:materialCircularityIndicator ;
        sh:name "materialCircularityIndicator"@en ;
        sh:description "Overall MCI score (0-1)"@en ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:maxInclusive 1 ;
        sh:maxCount 1 ;
        sh:message "materialCircularityIndicator must be a decimal between 0 and 1"@en
    ] ;

    # The three information carriers below are owl:DatatypeProperty with
    # rdfs:range xsd:anyURI, so their values are anyURI LITERALS, not IRI nodes
    # — sh:nodeKind sh:IRI would contradict the ontology and the shipped context.
    sh:property [
        sh:path oec:endOfLifeInstructions ;
        sh:name "endOfLifeInstructions"@en ;
        sh:datatype xsd:anyURI ;
        sh:maxCount 1
    ] ;

    sh:property [
        sh:path oec:wastePreventionInfo ;
        sh:name "wastePreventionInfo"@en ;
        sh:datatype xsd:anyURI ;
        sh:maxCount 1
    ] ;

    sh:property [
        sh:path oec:separateCollectionInfo ;
        sh:name "separateCollectionInfo"@en ;
        sh:datatype xsd:anyURI ;
        sh:maxCount 1
    ] .

# =============================================================================
# RecycledContent Shape
# =============================================================================

dpp-sh:RecycledContentShape
    a sh:NodeShape ;
    sh:targetClass oec:RecycledContent ;
    sh:name "Recycled Content Shape"@en ;
    sh:description "Validates RecycledContent instances"@en ;

    sh:property [
        sh:path oec:recycledContent ;
        sh:name "recycledContent"@en ;
        sh:description "Total fraction of recycled content (0-1)"@en ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:maxInclusive 1 ;
        sh:maxCount 1 ;
        sh:message "recycledContent must be a decimal between 0 and 1"@en
    ] ;

    sh:property [
        sh:path oec:preConsumerRecycledContent ;
        sh:name "preConsumerRecycledContent"@en ;
        sh:description "Fraction of pre-consumer recycled content (0-1)"@en ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:maxInclusive 1 ;
        sh:maxCount 1 ;
        sh:message "preConsumerRecycledContent must be a decimal between 0 and 1"@en
    ] ;

    sh:property [
        sh:path oec:postConsumerRecycledContent ;
        sh:name "postConsumerRecycledContent"@en ;
        sh:description "Fraction of post-consumer recycled content (0-1)"@en ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:maxInclusive 1 ;
        sh:maxCount 1 ;
        sh:message "postConsumerRecycledContent must be a decimal between 0 and 1"@en
    ] .

# =============================================================================
# MaterialComposition Shape
# =============================================================================

dpp-sh:MaterialCompositionShape
    a sh:NodeShape ;
    sh:targetClass oec:MaterialComposition ;
    sh:name "Material Composition Shape"@en ;
    sh:description "Validates MaterialComposition instances"@en ;

    sh:property [
        sh:path schema:name ;
        sh:name "materialName"@en ;
        sh:description "Name of the material"@en ;
        sh:node dpp-sh:TranslatableText ;
        sh:minCount 1 ;
        sh:message "materialName is required and must be a string"@en
    ] ;

    sh:property [
        sh:path oec:massFraction ;
        sh:name "massFraction"@en ;
        sh:description "Mass fraction of this material (0-1)"@en ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:maxInclusive 1 ;
        sh:maxCount 1 ;
        sh:message "massFraction must be a decimal between 0 and 1"@en
    ] ;

    sh:property [
        sh:path gs1:countryOfOrigin ;
        sh:name "sourceCountry"@en ;
        sh:description "Country of origin (ISO 3166-1 alpha-2)"@en ;
        sh:datatype xsd:string ;
        sh:pattern "^[A-Z]{2}$" ;
        sh:message "sourceCountry must be a 2-letter ISO country code (e.g., DE, FR, US)"@en
    ] ;

    sh:property [
        sh:path oec:isCriticalRawMaterial ;
        sh:name "isCriticalRawMaterial"@en ;
        sh:datatype xsd:boolean ;
        sh:maxCount 1
    ] ;

    sh:property [
        sh:path oec:casNumber ;
        sh:name "casNumber"@en ;
        sh:description "CAS Registry Number"@en ;
        sh:datatype xsd:string ;
        sh:pattern "^[0-9]{2,7}-[0-9]{2}-[0-9]$" ;
        sh:message "casNumber must be in CAS format (e.g., 7440-50-8)"@en
    ] .

# =============================================================================
# EmissionsPerformance Shape
# =============================================================================

dpp-sh:EmissionsPerformanceShape
    a sh:NodeShape ;
    sh:targetClass oec:EmissionsPerformance ;
    sh:name "Emissions Performance Shape"@en ;
    sh:description "Validates EmissionsPerformance instances"@en ;

    sh:property [
        sh:path oec:carbonFootprintTotal ;
        sh:name "carbonFootprintTotal"@en ;
        sh:description "Total carbon footprint in kg CO2e"@en ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:maxCount 1 ;
        sh:message "carbonFootprintTotal must be a non-negative decimal"@en
    ] ;

    sh:property [
        sh:path oec:declaredUnit ;
        sh:name "declaredUnit"@en ;
        sh:description "Functional unit for the measurement"@en ;
        sh:datatype xsd:string ;
        sh:maxCount 1
    ] ;

    sh:property [
        sh:path oec:hasOperationalScope ;
        sh:name "operationalScope"@en ;
        sh:description "Lifecycle boundary"@en ;
        sh:in ( oec:CradleToGate oec:CradleToGrave ) ;
        sh:maxCount 1 ;
        sh:message "operationalScope must be CradleToGate or CradleToGrave"@en
    ] ;

    sh:property [
        sh:path oec:primarySourcedRatio ;
        sh:name "primarySourcedRatio"@en ;
        sh:description "Fraction from direct measurement (0-1)"@en ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:maxInclusive 1 ;
        sh:maxCount 1 ;
        sh:message "primarySourcedRatio must be a decimal between 0 and 1"@en
    ] .

# =============================================================================
# TraceabilityPerformance Shape
# =============================================================================

dpp-sh:TraceabilityPerformanceShape
    a sh:NodeShape ;
    sh:targetClass oec:TraceabilityPerformance ;
    sh:name "Traceability Performance Shape"@en ;
    sh:description "Validates TraceabilityPerformance instances"@en ;

    sh:property [
        sh:path oec:verifiedRatio ;
        sh:name "verifiedRatio"@en ;
        sh:description "Fraction of materials verifiably traced (0-1)"@en ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:maxInclusive 1 ;
        sh:maxCount 1 ;
        sh:message "verifiedRatio must be a decimal between 0 and 1"@en
    ] .

# =============================================================================
# OperatorInformation Shape
# =============================================================================

dpp-sh:OperatorInformationShape
    a sh:NodeShape ;
    sh:targetClass oec:OperatorInformation ;
    sh:name "Operator Information Shape"@en ;
    sh:description "Validates OperatorInformation instances"@en ;

    # The role must be one of the seven ESPR Art. 77 economic-operator roles
    # WHEREVER it is stated. Whether it is REQUIRED is a separate question, and
    # the answer depends on why the node is in the graph — see
    # dpp-sh:EconomicOperatorRoleRequired below.
    sh:property [
        sh:path oec:hasOperatorRole ;
        sh:name "operatorRole"@en ;
        sh:description "Role of the economic operator"@en ;
        sh:in (
            oec:Manufacturer
            oec:Importer
            oec:Distributor
            oec:Processor
            oec:Trader
            oec:AuthorisedRepresentative
            oec:FulfilmentServiceProvider
        ) ;
        sh:message "operatorRole must be one of the seven ESPR Art. 77 economic-operator roles"@en
    ] ;

    sh:property [
        sh:path gs1:globalLocationNumber ;
        sh:name "gln"@en ;
        sh:description "Operator GLN (Global Location Number)"@en ;
        sh:datatype xsd:string ;
        sh:pattern "^[0-9]{13}$" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "gln must be a 13-digit GLN"@en
    ] ;

    sh:property [
        sh:path oec:economicOperatorId ;
        sh:name "economicOperatorId"@en ;
        sh:description "EU EOID per ESPR Article 77"@en ;
        sh:datatype xsd:string ;
        sh:maxCount 1
    ] ;

    sh:property [
        sh:path oec:eoriNumber ;
        sh:name "eoriNumber"@en ;
        sh:description "EORI number for customs"@en ;
        sh:datatype xsd:string ;
        sh:pattern "^[A-Z]{2}[A-Z0-9]{1,15}$" ;
        sh:message "eoriNumber must be in EORI format (e.g., DE123456789012345)"@en
    ] ;

    sh:property [
        sh:path schema:vatID ;
        sh:name "vatIdentificationNumber"@en ;
        sh:description "VAT identification number"@en ;
        sh:datatype xsd:string ;
        sh:maxCount 1
    ] .

# =============================================================================
# FacilityInformation Shape
# =============================================================================

dpp-sh:FacilityInformationShape
    a sh:NodeShape ;
    sh:targetClass oec:FacilityInformation ;
    sh:name "Facility Information Shape"@en ;
    sh:description "Validates FacilityInformation instances"@en ;

    sh:property [
        sh:path gs1:globalLocationNumber ;
        sh:name "gln"@en ;
        sh:description "Facility GLN (Global Location Number)"@en ;
        sh:datatype xsd:string ;
        sh:pattern "^[0-9]{13}$" ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "gln must be a 13-digit GLN"@en
    ] ;

    sh:property [
        sh:path schema:name ;
        sh:name "name"@en ;
        sh:description "Official facility name (inherited from gs1:Place)"@en ;
        sh:node dpp-sh:TranslatableText ;
        sh:minCount 1 ;
        sh:message "name is required"@en
    ] ;

    sh:property [
        sh:path oec:facilityType ;
        sh:name "facilityType"@en ;
        sh:datatype xsd:string ;
        sh:maxCount 1
    ] .

# =============================================================================
# SubstanceOfConcern Shape
# =============================================================================

dpp-sh:SubstanceOfConcernShape
    a sh:NodeShape ;
    sh:targetClass oec:SubstanceOfConcern ;
    sh:name "Substance of Concern Shape"@en ;
    sh:description "Validates SubstanceOfConcern instances"@en ;

    sh:property [
        sh:path schema:name ;
        sh:name "substanceName"@en ;
        sh:description "Name of the hazardous substance"@en ;
        sh:node dpp-sh:TranslatableText ;
        sh:minCount 1 ;
        sh:message "substanceName is required"@en
    ] ;

    sh:property [
        sh:path oec:casNumber ;
        sh:name "casNumber"@en ;
        sh:description "CAS Registry Number"@en ;
        sh:datatype xsd:string ;
        sh:pattern "^[0-9]{2,7}-[0-9]{2}-[0-9]$" ;
        sh:message "casNumber must be in CAS format (e.g., 7440-50-8)"@en
    ] ;

    sh:property [
        sh:path oec:ecNumber ;
        sh:name "ecNumber"@en ;
        sh:description "EC Number (EINECS/ELINCS)"@en ;
        sh:datatype xsd:string ;
        sh:pattern "^[0-9]{3}-[0-9]{3}-[0-9]$" ;
        sh:message "ecNumber must be in EC format (e.g., 231-111-4)"@en
    ] ;

    sh:property [
        sh:path oec:concentration ;
        sh:name "concentration"@en ;
        sh:description "Concentration as fraction (0-1)"@en ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:maxInclusive 1 ;
        sh:maxCount 1 ;
        sh:message "concentration must be a decimal between 0 and 1"@en
    ] ;

    sh:property [
        sh:path oec:scipId ;
        sh:name "scipId"@en ;
        sh:description "ECHA SCIP database identifier"@en ;
        sh:datatype xsd:string ;
        sh:maxCount 1
    ] ;

    sh:property [
        sh:path oec:hasHazardClass ;
        sh:name "hazardClass"@en ;
        sh:in (
            oec:AcuteToxicity
            oec:SkinCorrosionOrIrritation
            oec:EyeDamageOrIrritation
            oec:RespiratoryOrSkinSensitization
            oec:GermCellMutagenicity
            oec:Carcinogenicity
            oec:ReproductiveToxicity
            oec:SpecificTargetOrganToxicity
            oec:AspirationHazard
            oec:HazardousToAquaticEnvironment
        ) ;
        sh:message "hazardClass must be a valid HazardClass"@en
    ] .

# =============================================================================
# AccessRights Shape
# =============================================================================

dpp-sh:AccessRightsShape
    a sh:NodeShape ;
    sh:targetClass oec:AccessRights ;
    sh:name "Access Rights Shape"@en ;
    sh:description "Validates AccessRights instances"@en ;

    sh:property [
        sh:path oec:hasAccessLevel ;
        sh:name "accessLevel"@en ;
        sh:description "ESPR Article 9 access level"@en ;
        sh:in ( oec:Public oec:AuthorizedOnly oec:Restricted ) ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "accessLevel is required and must be Public, AuthorizedOnly, or Restricted"@en
    ] .

# =============================================================================
# RepairabilityInfo Shape
# =============================================================================

dpp-sh:RepairabilityInfoShape
    a sh:NodeShape ;
    sh:targetClass oec:RepairabilityInfo ;
    sh:name "Repairability Information Shape"@en ;
    sh:description "Validates RepairabilityInfo instances"@en ;

    sh:property [
        sh:path oec:repairabilityScore ;
        sh:name "repairabilityScore"@en ;
        sh:description "Repairability score (typically 0-10)"@en ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:maxInclusive 10 ;
        sh:maxCount 1 ;
        sh:message "repairabilityScore must be between 0 and 10"@en
    ] ;

    sh:property [
        sh:path oec:repairabilityClass ;
        sh:name "repairabilityClass"@en ;
        sh:description "Repairability class (A-E)"@en ;
        sh:datatype xsd:string ;
        sh:pattern "^[A-E]$" ;
        sh:maxCount 1 ;
        sh:message "repairabilityClass must be A, B, C, D, or E"@en
    ] ;

    sh:property [
        sh:path oec:diyRepairPossible ;
        sh:name "diyRepairPossible"@en ;
        sh:datatype xsd:boolean ;
        sh:maxCount 1
    ] ;

    sh:property [
        sh:path oec:professionalRepairNetwork ;
        sh:name "professionalRepairNetwork"@en ;
        # owl:DatatypeProperty, rdfs:range xsd:anyURI — a literal, not an IRI node.
        sh:datatype xsd:anyURI ;
        sh:maxCount 1
    ] .

# =============================================================================
# PerformanceInfo Shape
# =============================================================================

dpp-sh:PerformanceInfoShape
    a sh:NodeShape ;
    sh:targetClass oec:PerformanceInfo ;
    sh:name "Performance Information Shape"@en ;
    sh:description "Validates PerformanceInfo instances"@en ;

    sh:property [
        sh:path oec:usageCycles ;
        sh:name "usageCycles"@en ;
        sh:description "Expected number of usage cycles"@en ;
        sh:datatype xsd:integer ;
        sh:minInclusive 0 ;
        sh:maxCount 1 ;
        sh:message "usageCycles must be a non-negative integer"@en
    ] ;

    sh:property [
        sh:path oec:performanceClass ;
        sh:name "performanceClass"@en ;
        sh:description "Performance/efficiency class (e.g., A+++ to G)"@en ;
        sh:datatype xsd:string ;
        sh:maxCount 1
    ] .

# =============================================================================
# GranularityLevel Constraint
# =============================================================================

dpp-sh:GranularityLevelConstraint
    a sh:NodeShape ;
    sh:name "Granularity Level Constraint"@en ;
    sh:property [
        sh:path oec:granularityLevel ;
        sh:datatype xsd:string ;
        sh:in ( "model" "batch" "item" ) ;
        sh:maxCount 1 ;
        sh:message "granularity must be model, batch, or item (EN 18223)"@en
    ] .

# =============================================================================
# DigitalProductPassport: granularity must match the GS1 Digital Link AIs
# (EN 18219 4.4 consistency; EN 18223 granularity). 01 -> model,
# 01+10 -> batch, 01+21 -> item. Applies when uniqueProductIdentifier
# (gs1:productID) is a GS1 Digital Link URI.
# =============================================================================

# Target: every node that DECLARES a granularity. The previous version targeted
# oec:DigitalProductPassport and read the Digital Link from gs1:productID, and
# neither appears anywhere in the corpus — no example is typed as that class and
# none sets that property, so this constraint had never fired on anything. Eleven
# examples declare oec:granularityLevel and carry the Digital Link as their own
# node IRI, which is how the resolver addresses them, so that is what to check.
#
# Only Apache Jena among the engines in play evaluates sh:sparql, so this is
# enforced by the ITB parity gate (pnpm run check:shapes:itb) and not by the
# in-process build gate. scripts/check-granularity.ts keeps the same invariant
# green in the ordinary offline build.
dpp-sh:GranularityDigitalLinkConstraint
    a sh:NodeShape ;
    sh:targetSubjectsOf oec:granularityLevel ;
    sh:sparql [
        a sh:SPARQLConstraint ;
        sh:message "granularity must match the GS1 Digital Link Application Identifiers of the passport IRI (01 -> model, 01+10 -> batch, 01+21 -> item) per EN 18219 4.4 / EN 18223."@en ;
        sh:prefixes <https://ref.openepcis.io/extensions/common/core/shapes/> ;
        sh:select """
            SELECT $this ?value WHERE {
                $this oec:granularityLevel ?value .
                # The Digital Link is the node's own IRI; gs1:productID is honoured
                # as an alternative carrier where a passport states it separately.
                OPTIONAL { $this gs1:productID ?stated }
                BIND( COALESCE(?stated, STR($this)) AS ?dl )
                FILTER( REGEX(STR(?dl), "/01/") )
                BIND( IF(REGEX(STR(?dl), "/21/"), "item",
                       IF(REGEX(STR(?dl), "/10/"), "batch", "model")) AS ?expected )
                FILTER( STR(?value) != ?expected )
            }
        """ ;
    ] .

# =============================================================================
# QuantitativeValue Shapes (GS1-idiomatic value + unitCode pattern)
# =============================================================================
# Every QuantitativeValue node must have a numeric `gs1:value` and a
# `gs1:unitCode` (UN/CEFACT Rec 20). Per-property NodeShapes pin the code.

dpp-sh:QuantitativeValueShape
    a sh:NodeShape ;
    sh:targetClass gs1:QuantitativeValue ;
    sh:name "QuantitativeValue Shape"@en ;
    sh:description "Every QuantitativeValue must have gs1:value and gs1:unitCode (UN/CEFACT Rec 20)."@en ;
    sh:property [
        sh:path gs1:value ;
        sh:datatype xsd:decimal ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "QuantitativeValue must have exactly one xsd:decimal gs1:value"@en
    ] ;
    sh:property [
        sh:path gs1:unitCode ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "QuantitativeValue must have exactly one gs1:unitCode (UN/CEFACT Rec 20 string)"@en
    ] .

# Per-property unitCode shapes (representative; extend as new properties land).
# Pattern: target subjects of a property; require its value's unitCode = canonical code.

dpp-sh:ExpectedLifespanShape
    a sh:NodeShape ;
    sh:targetSubjectsOf oec:hasExpectedLifespan ;
    sh:property [
        sh:path ( oec:hasExpectedLifespan gs1:unitCode ) ;
        sh:hasValue "ANN" ;
        sh:message "oec:hasExpectedLifespan must use unitCode 'ANN' (years)"@en
    ] .

dpp-sh:GuaranteedLifespanShape
    a sh:NodeShape ;
    sh:targetSubjectsOf oec:hasGuaranteedLifespan ;
    sh:property [
        sh:path ( oec:hasGuaranteedLifespan gs1:unitCode ) ;
        sh:hasValue "ANN" ;
        sh:message "oec:hasGuaranteedLifespan must use unitCode 'ANN' (years)"@en
    ] .

dpp-sh:TechnicalLifetimeShape
    a sh:NodeShape ;
    sh:targetSubjectsOf oec:hasTechnicalLifetime ;
    sh:property [
        sh:path ( oec:hasTechnicalLifetime gs1:unitCode ) ;
        sh:hasValue "ANN" ;
        sh:message "oec:hasTechnicalLifetime must use unitCode 'ANN' (years)"@en
    ] .

# =============================================================================
# Cross-Cutting EU Concept Shapes (lifted)
# =============================================================================

dpp-sh:ExtendedProducerResponsibilityShape
    a sh:NodeShape ;
    sh:targetClass oec:ExtendedProducerResponsibility ;
    sh:name "Extended Producer Responsibility Shape"@en ;
    sh:property [
        sh:path oec:eprRegistrationNumber ;
        sh:datatype xsd:string ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "ExtendedProducerResponsibility must have exactly one eprRegistrationNumber"@en
    ] ;
    sh:property [
        sh:path oec:hasEprJurisdiction ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "ExtendedProducerResponsibility must declare exactly one eprJurisdiction"@en
    ] ;
    sh:property [
        sh:path oec:eprComplianceUrl ;
        sh:datatype xsd:anyURI ;
        sh:maxCount 1
    ] .

dpp-sh:CompostabilityShape
    a sh:NodeShape ;
    sh:targetClass oec:Compostability ;
    sh:name "Compostability Shape"@en ;
    sh:property [
        sh:path oec:hasCompostabilityType ;
        sh:in ( oec:IndustrialCompostable oec:HomeCompostable oec:NotCompostable ) ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "compostabilityType must be IndustrialCompostable, HomeCompostable, or NotCompostable"@en
    ] ;
    sh:property [
        sh:path oec:compostabilityStandard ;
        sh:datatype xsd:anyURI ;
        sh:maxCount 1
    ] .

dpp-sh:BiodegradabilityShape
    a sh:NodeShape ;
    sh:targetClass oec:Biodegradability ;
    sh:name "Biodegradability Shape"@en ;
    sh:property [
        sh:path oec:biodegradationPercentage ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
        sh:message "Biodegradability must have exactly one biodegradationPercentage"@en
    ] ;
    sh:property [
        sh:path oec:hasBiodegradabilityTestMethod ;
        sh:in ( oec:ISO14593 oec:OECD301B oec:OECD301D oec:OECD301F oec:OECD310 ) ;
        sh:maxCount 1
    ] .

dpp-sh:DepositReturnSchemeShape
    a sh:NodeShape ;
    sh:targetClass oec:DepositReturnScheme ;
    sh:name "DepositReturnScheme Shape"@en ;
    # A deposit amount must be declared, in EITHER of the two forms the model
    # deliberately keeps apart.
    #
    # oec:hasDepositAmount ranges over gs1:QuantitativeValue; the GS1 term
    # gs1:returnablePackageDepositAmount ranges over gs1:PriceSpecification, which
    # carries a currency. The ontology records the difference itself — the
    # skos:note on oec:hasDepositAmount calls it a closeMatch, "same meaning,
    # different value structure" — so neither subsumes the other and a
    # rdfs:subPropertyOf axiom would be false. For a monetary deposit the GS1
    # form is the better fit and the delegation rule prefers it, so requiring the
    # oec: spelling alone would reject the more correct document.
    sh:or (
        [
            sh:property [
                sh:path oec:hasDepositAmount ;
                sh:minCount 1 ;
                sh:maxCount 1
            ]
        ]
        [
            sh:property [
                sh:path gs1:returnablePackageDepositAmount ;
                sh:minCount 1 ;
                sh:maxCount 1
            ]
        ]
    ) ;
    sh:message "DepositReturnScheme must declare exactly one deposit amount, as oec:hasDepositAmount (gs1:QuantitativeValue) or gs1:returnablePackageDepositAmount (gs1:PriceSpecification)"@en ;
    sh:property [
        sh:path oec:depositRedemptionChannelUrl ;
        sh:datatype xsd:anyURI ;
        sh:maxCount 1
    ] .

# Constrain biodegradationPercentage to the 0-100 percent range.
#
# This shape previously pinned ( oec:biodegradationPercentage gs1:unitCode ) to
# "P1", by analogy with the has* lifespan shapes above. That analogy does not
# hold: those properties are owl:ObjectProperty ranging over
# gs1:QuantitativeValue, whereas oec:biodegradationPercentage is an
# owl:DatatypeProperty with rdfs:range xsd:decimal. A decimal literal has no
# gs1:unitCode, so the constraint could never be satisfied by any document the
# ontology permits — it simply never fired, because until the detergent module's
# specialised property was subordinated to it nothing used the core term at all.
# The meaningful check on a bare percentage is its value range.
dpp-sh:BiodegradationPercentageRangeShape
    a sh:NodeShape ;
    sh:targetSubjectsOf oec:biodegradationPercentage ;
    sh:property [
        sh:path oec:biodegradationPercentage ;
        sh:datatype xsd:decimal ;
        sh:minInclusive 0 ;
        sh:maxInclusive 100 ;
        sh:message "oec:biodegradationPercentage must be a decimal percentage between 0 and 100"@en
    ] .
