Skip to content

Commit 43aa79f

Browse files
dfcoffinclaude
andcommitted
feat: ESPI 4.0 Schema Compliance - Phase 14: Subscription
Subscription is an application-specific entity (NOT an ESPI resource), so it should not extend IdentifiedObject. This change removes the inheritance and updates the entity to use a simple UUID primary key. Key changes: - SubscriptionEntity no longer extends IdentifiedObject - Removed description, created, updated, published, selfLink, upLink fields - Added UUID5 generation via EspiIdGeneratorService.generateSubscriptionId() - Created SubscriptionDto for Atom Feed output with dynamic certification links - Created SubscriptionMapper for Entity-to-DTO conversion with @value injection - Simplified SubscriptionRepository to use Spring Data JPA query derivation - Updated Flyway V1 migration to match simplified entity structure - Added greenbutton.* properties for certification configuration - Updated all related tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a17e725 commit 43aa79f

13 files changed

Lines changed: 1135 additions & 438 deletions

File tree

openespi-common/src/main/java/org/greenbuttonalliance/espi/common/domain/usage/SubscriptionEntity.java

Lines changed: 61 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -24,51 +24,62 @@
2424
import lombok.Getter;
2525
import lombok.NoArgsConstructor;
2626
import lombok.Setter;
27-
import org.greenbuttonalliance.espi.common.domain.common.IdentifiedObject;
27+
import org.hibernate.annotations.JdbcTypeCode;
2828
import org.hibernate.proxy.HibernateProxy;
29+
import org.hibernate.type.SqlTypes;
2930

30-
import java.time.Instant;
31-
import java.time.LocalDateTime;
32-
import java.time.ZoneOffset;
31+
import java.io.Serializable;
3332
import java.util.ArrayList;
3433
import java.util.List;
3534
import java.util.Objects;
35+
import java.util.UUID;
3636

3737
/**
3838
* Pure JPA/Hibernate entity for Subscription without JAXB concerns.
39-
*
39+
*
4040
* Defines the parameters of a subscription between Third Party and Data
4141
* Custodian. Represents a formal agreement allowing third-party applications
4242
* to access specific usage points and energy data for a retail customer.
43+
*
44+
* <p>Key characteristics:</p>
45+
* <ul>
46+
* <li>Application-specific entity (NOT an ESPI standard resource)</li>
47+
* <li>Uses UUID for primary key (indexed for API access)</li>
48+
* <li>Links OAuth2 Authorization to accessible UsagePoints</li>
49+
* <li>Does NOT extend IdentifiedObject (no selfLink/upLink in database)</li>
50+
* <li>Atom Feed output handled by DTO layer with dynamic links</li>
51+
* </ul>
4352
*/
4453
@Entity
4554
@Table(name = "subscriptions", indexes = {
4655
@Index(name = "idx_subscription_retail_customer", columnList = "retail_customer_id"),
4756
@Index(name = "idx_subscription_application", columnList = "application_information_id"),
48-
@Index(name = "idx_subscription_authorization", columnList = "authorization_id"),
49-
@Index(name = "idx_subscription_last_update", columnList = "last_update")
57+
@Index(name = "idx_subscription_authorization", columnList = "authorization_id")
5058
})
5159
@Getter
5260
@Setter
5361
@NoArgsConstructor
54-
public class SubscriptionEntity extends IdentifiedObject {
62+
public class SubscriptionEntity implements Serializable {
5563

5664
private static final long serialVersionUID = 1L;
5765

66+
/**
67+
* UUID primary key for API access.
68+
* Subscription is an application-specific entity, not an ESPI resource,
69+
* but uses UUID for consistent API patterns.
70+
*/
71+
@Id
72+
@JdbcTypeCode(SqlTypes.CHAR)
73+
@Column(length = 36, columnDefinition = "char(36)", updatable = false, nullable = false)
74+
private UUID id;
75+
5876
/**
5977
* Optional hashed identifier for external references.
6078
* Used for privacy and security in external communications.
6179
*/
6280
@Column(name = "hashed_id", length = 64)
6381
private String hashedId;
6482

65-
/**
66-
* Last update timestamp for this subscription.
67-
* Tracks when the subscription configuration was last modified.
68-
*/
69-
@Column(name = "last_update")
70-
private LocalDateTime lastUpdate;
71-
7283
/**
7384
* Retail customer who owns this subscription.
7485
* The customer whose data is being accessed through this subscription.
@@ -107,149 +118,42 @@ public class SubscriptionEntity extends IdentifiedObject {
107118
)
108119
private List<UsagePointEntity> usagePoints = new ArrayList<>();
109120

121+
/**
122+
* Constructor with UUID.
123+
* UUID5 should be generated by EspiIdGeneratorService.generateSubscriptionId().
124+
*
125+
* @param id the UUID5 identifier (must be provided, not generated here)
126+
*/
127+
public SubscriptionEntity(UUID id) {
128+
this.id = id;
129+
}
130+
110131
/**
111132
* Constructor with basic subscription information.
112-
*
133+
* Note: ID must be set separately using UUID5 from EspiIdGeneratorService.
134+
*
113135
* @param retailCustomer the customer who owns the subscription
114136
* @param applicationInformation the application accessing the data
115137
*/
116138
public SubscriptionEntity(RetailCustomerEntity retailCustomer, ApplicationInformationEntity applicationInformation) {
117139
this.retailCustomer = retailCustomer;
118140
this.applicationInformation = applicationInformation;
119-
this.lastUpdate = LocalDateTime.now();
120-
}
121-
122-
// Note: Simple setter for authorization is generated by Lombok @Data
123-
// Complex bidirectional relationship management removed - handled by DataCustodian/ThirdParty applications
124-
125-
// Note: Usage point collection accessors are generated by Lombok @Data
126-
// Bidirectional relationship management methods removed - handled by DataCustodian/ThirdParty applications
127-
128-
/**
129-
* Updates the last update timestamp to current time.
130-
*/
131-
public void updateLastUpdate() {
132-
this.lastUpdate = LocalDateTime.now();
133-
}
134-
135-
/**
136-
* Gets the last update time as LocalDateTime.
137-
*
138-
* @return last update as LocalDateTime, or null if not set
139-
*/
140-
public LocalDateTime getLastUpdateAsLocalDateTime() {
141-
if (lastUpdate == null) {
142-
return null;
143-
}
144-
return lastUpdate;
145-
}
146-
147-
/**
148-
* Sets the last update time from LocalDateTime.
149-
*
150-
* @param dateTime the LocalDateTime to set
151-
*/
152-
public void setLastUpdateFromLocalDateTime(LocalDateTime dateTime) {
153-
this.lastUpdate = dateTime;
154-
}
155-
156-
/**
157-
* Gets the last update time as Instant.
158-
*
159-
* @return last update as Instant, or null if not set
160-
*/
161-
public Instant getLastUpdateAsInstant() {
162-
return lastUpdate != null ? lastUpdate.toInstant(ZoneOffset.UTC): null;
163-
}
164-
165-
/**
166-
* Generates the self href for this subscription.
167-
*
168-
* @return self href string
169-
*/
170-
public String getSelfHref() {
171-
return "/espi/1_1/resource/Subscription/" + getHashedId();
172141
}
173142

174143
/**
175-
* Generates the up href for this subscription.
176-
*
177-
* @return up href string
144+
* Gets a string representation of the ID for href generation.
145+
*
146+
* @return string representation of the UUID
178147
*/
179-
public String getUpHref() {
180-
return "/espi/1_1/resource/Subscription";
181-
}
182-
183-
/**
184-
* Overrides the default self href generation to use subscription specific logic.
185-
*
186-
* @return self href for this subscription
187-
*/
188-
@Override
189-
protected String generateDefaultSelfHref() {
190-
return getSelfHref();
191-
}
192-
193-
/**
194-
* Overrides the default up href generation to use subscription specific logic.
195-
*
196-
* @return up href for this subscription
197-
*/
198-
@Override
199-
protected String generateDefaultUpHref() {
200-
return getUpHref();
201-
}
202-
203-
/**
204-
* Merges data from another SubscriptionEntity.
205-
* Updates subscription parameters while preserving critical relationships.
206-
*
207-
* @param other the other subscription entity to merge from
208-
*/
209-
public void merge(SubscriptionEntity other) {
210-
if (other != null) {
211-
super.merge(other);
212-
213-
// Update basic fields
214-
this.hashedId = other.hashedId;
215-
this.lastUpdate = other.lastUpdate;
216-
217-
// Update relationships if provided
218-
if (other.applicationInformation != null) {
219-
this.applicationInformation = other.applicationInformation;
220-
}
221-
if (other.authorization != null) {
222-
this.authorization = other.authorization;
223-
}
224-
if (other.retailCustomer != null) {
225-
this.retailCustomer = other.retailCustomer;
226-
}
227-
if (other.usagePoints != null) {
228-
this.usagePoints = new ArrayList<>(other.usagePoints);
229-
}
230-
}
231-
}
232-
233-
/**
234-
* Clears all relationships when unlinking the entity.
235-
* Simplified - applications handle relationship cleanup.
236-
*/
237-
public void unlink() {
238-
clearRelatedLinks();
239-
240-
// Simple collection clearing - applications handle bidirectional cleanup
241-
usagePoints.clear();
242-
243-
// Clear authorization with simple field assignment
244-
this.authorization = null;
245-
246-
// Note: We don't clear retailCustomer or applicationInformation as they might be referenced elsewhere
148+
public String getHashedId() {
149+
// Return the explicit hashedId if set, otherwise use UUID string
150+
return hashedId != null ? hashedId : (id != null ? id.toString() : null);
247151
}
248152

249153
/**
250154
* Checks if this subscription is active.
251155
* A subscription is active if it has an active authorization.
252-
*
156+
*
253157
* @return true if subscription is active, false otherwise
254158
*/
255159
public boolean isActive() {
@@ -259,7 +163,7 @@ public boolean isActive() {
259163
/**
260164
* Checks if this subscription has expired.
261165
* A subscription is expired if its authorization has expired.
262-
*
166+
*
263167
* @return true if subscription is expired, false otherwise
264168
*/
265169
public boolean isExpired() {
@@ -269,7 +173,7 @@ public boolean isExpired() {
269173
/**
270174
* Checks if this subscription is revoked.
271175
* A subscription is revoked if its authorization is revoked.
272-
*
176+
*
273177
* @return true if subscription is revoked, false otherwise
274178
*/
275179
public boolean isRevoked() {
@@ -278,7 +182,7 @@ public boolean isRevoked() {
278182

279183
/**
280184
* Gets the number of usage points in this subscription.
281-
*
185+
*
282186
* @return count of usage points
283187
*/
284188
public int getUsagePointCount() {
@@ -287,26 +191,18 @@ public int getUsagePointCount() {
287191

288192
/**
289193
* Checks if this subscription includes the specified usage point.
290-
*
194+
*
291195
* @param usagePoint the usage point to check
292196
* @return true if included, false otherwise
293197
*/
294198
public boolean includesUsagePoint(UsagePointEntity usagePoint) {
295199
return usagePoints != null && usagePoints.contains(usagePoint);
296200
}
297201

298-
/**
299-
* Checks if this subscription includes a usage point with the specified ID.
300-
*
301-
* @param usagePointId the usage point ID to check
302-
* @return true if included, false otherwise
303-
*/
304-
// Note: includesUsagePointId() method removed - applications can implement custom lookup logic
305-
306202
/**
307203
* Gets the subscription ID from a resource URI.
308204
* Extracts the ID from URI patterns like "/espi/1_1/resource/Subscription/{id}".
309-
*
205+
*
310206
* @param resourceURI the resource URI
311207
* @return subscription ID, or null if not found
312208
*/
@@ -320,41 +216,29 @@ public static String getSubscriptionIdFromUri(String resourceURI) {
320216

321217
/**
322218
* Checks if this subscription belongs to the specified customer.
323-
*
219+
*
324220
* @param customerId the customer ID to check
325221
* @return true if belongs to customer, false otherwise
326222
*/
327223
public boolean belongsToCustomer(Long customerId) {
328-
return retailCustomer != null && customerId != null &&
224+
return retailCustomer != null && customerId != null &&
329225
customerId.equals(retailCustomer.getId());
330226
}
331227

332228
/**
333-
* Checks if this subscription is for the specified application.
334-
*
335-
* @param applicationId the application ID to check
336-
* @return true if for the application, false otherwise
337-
*/
338-
// Note: isForApplication() method removed - applications can implement custom lookup logic
339-
340-
/**
341-
* Pre-persist callback to set default values.
229+
* Pre-persist callback to validate required fields.
230+
* UUID5 must be set by the service layer before persisting.
231+
*
232+
* @throws IllegalStateException if ID is not set
342233
*/
343234
@PrePersist
344235
protected void onCreate() {
345-
if (lastUpdate == null) {
346-
lastUpdate = LocalDateTime.now();
236+
if (id == null) {
237+
throw new IllegalStateException(
238+
"Subscription ID must be set using EspiIdGeneratorService.generateSubscriptionId() before persisting");
347239
}
348240
}
349241

350-
/**
351-
* Pre-update callback to update the last update timestamp.
352-
*/
353-
@PreUpdate
354-
protected void onUpdate() {
355-
updateLastUpdate();
356-
}
357-
358242
@Override
359243
public final boolean equals(Object o) {
360244
if (this == o) return true;
@@ -375,11 +259,6 @@ public final int hashCode() {
375259
public String toString() {
376260
return getClass().getSimpleName() + "(" +
377261
"id = " + getId() + ", " +
378-
"hashedId = " + getHashedId() + ", " +
379-
"lastUpdate = " + getLastUpdate() + ", " +
380-
"description = " + getDescription() + ", " +
381-
"created = " + getCreated() + ", " +
382-
"updated = " + getUpdated() + ", " +
383-
"published = " + getPublished() + ")";
262+
"hashedId = " + getHashedId() + ")";
384263
}
385-
}
264+
}

0 commit comments

Comments
 (0)