Agreement Management Deep Dive
Overview
This comprehensive guide covers Master Service Agreement (MSA) management in the Ink platform, including agreement lifecycle, approval workflows, renewal tracking, amendment handling, and integration with projects and subscriptions.
Target Audience: Backend developers working with contracts and agreements
Prerequisites: Service Layer Architecture, Customer Management
Estimated Time: 50-60 minutes
Prerequisites
- Understanding of contract management concepts
- Knowledge of approval workflows
- Familiarity with Integration Patterns
- Completed Service Layer Architecture
- Understanding of Customer Management
Agreement Lifecycle Architecture
Installation Steps
The Installation Steps section establishes agreement management capabilities including contract lifecycle tracking, approval workflows, renewal notifications, and integration with customer projects.
What You'll Set Up:
- Agreement Dependencies - No additional dependencies (uses core JPA)
- Agreement Configuration - Workflow settings and notification policies
Why This Matters:
- Centralizes contract management and compliance tracking
- Automates approval workflows and renewal notifications
- Links agreements to projects, subscriptions, and orders
- Provides audit trail for all agreement changes
- Supports multi-party agreements with complex terms
When to Use:
- Initial application setup
- Adding contract management features
- Implementing approval workflows
- Tracking customer commitments
Key Capabilities Enabled:
- Agreement CRUD: Create, read, update, delete agreement records
- Lifecycle Management: Track agreements through draft, approval, execution, renewal
- Approval Workflows: Multi-step approval processes
- Amendment Tracking: Manage agreement amendments and versions
- Renewal Management: Automated renewal notifications and tracking
- Integration: Link agreements to customers, projects, and subscriptions
Agreement Hierarchy:
agreement
├── approval
│ ├── requireApproval: true
│ └── autoApproveThreshold: 10000
├── renewal
│ ├── autoRenew: false
│ ├── notificationDays: [90, 60, 30, 15]
│ └── gracePeriodDays: 30
├── amendment
│ ├── versioningEnabled: true
│ └── requireSignature: true
└── validation
├── requireProjectLink: false
└── requireCustomerActive: true
1. Agreement Dependencies
The Agreement Service orchestrates all agreement operations including CRUD, approval workflows, renewal tracking, amendment management, and integration with projects and subscriptions.
Why Is It Needed?
- Business Logic: Encapsulate agreement management rules and workflows
- Approval Orchestration: Manage multi-step approval processes
- Lifecycle Management: Track agreements through their entire lifecycle
- Integration: Coordinate with customer, project, and subscription services
- Compliance: Ensure all operations follow business policies
How/When Is It Used?
- Agreement Creation: New customer contracts
- Approval Processing: Routing agreements through approval workflow
- Renewal Management: Tracking and processing renewals
- Amendment Recording: Managing changes to existing agreements
- Project Authorization: Validating projects against agreement scope
Service Operation Flow
Code Example (Java)
// filepath: /Users/jetstart/dev/jetrev/ink/src/main/java/com/jetrev/ink/service/agreement/AgreementService.java
@Service
@Transactional
@RequiredArgsConstructor
@Slf4j
public class AgreementService {
private final AgreementRepository agreementRepository;
private final CustomerRepository customerRepository;
private final ApprovalWorkflowService approvalWorkflowService;
private final AgreementEventProducer eventProducer;
private final AgreementMapper agreementMapper;
@Value("${agreement.approval.requireApproval:true}")
private boolean requireApproval;
@Value("${agreement.approval.autoApproveThreshold:10000}")
private BigDecimal autoApproveThreshold;
public AgreementDto createAgreement(CreateAgreementRequest request) {
log.info("Creating agreement for customer: {}", request.getCustomerId());
Customer customer = customerRepository.findById(request.getCustomerId())
.orElseThrow(() -> new CustomerNotFoundException(request.getCustomerId()));
Agreement agreement = Agreement.builder()
.customer(customer)
.title(request.getTitle())
.description(request.getDescription())
.startDate(request.getStartDate())
.endDate(request.getEndDate())
.totalValue(request.getTotalValue())
.paymentTerms(request.getPaymentTerms())
.termsAndConditions(request.getTermsAndConditions())
.status(AgreementStatus.DRAFT)
.build();
Agreement saved = agreementRepository.save(agreement);
// Initialize approval workflow if required
if (requireApproval) {
approvalWorkflowService.initializeWorkflow(saved);
}
eventProducer.publishAgreementCreatedEvent(saved);
log.info("Agreement created: id={}, number={}",
saved.getId(), saved.getAgreementNumber());
return agreementMapper.toDto(saved);
}
public AgreementDto submitForApproval(Long agreementId) {
log.info("Submitting agreement for approval: id={}", agreementId);
Agreement agreement = agreementRepository.findById(agreementId)
.orElseThrow(() -> new AgreementNotFoundException(agreementId));
if (agreement.getStatus() != AgreementStatus.DRAFT) {
throw new IllegalStateException("Only draft agreements can be submitted for approval");
}
// Check if auto-approval applies
if (agreement.getTotalValue().compareTo(autoApproveThreshold) < 0) {
return autoApprove(agreement);
}
agreement.setStatus(AgreementStatus.PENDING_APPROVAL);
Agreement updated = agreementRepository.save(agreement);
approvalWorkflowService.submitForApproval(updated);
eventProducer.publishAgreementSubmittedEvent(updated);
return agreementMapper.toDto(updated);
}
public AgreementDto approveAgreement(Long agreementId, Long approverId) {
log.info("Approving agreement: id={}, approverId={}", agreementId, approverId);
Agreement agreement = agreementRepository.findById(agreementId)
.orElseThrow(() -> new AgreementNotFoundException(agreementId));
User approver = userRepository.findById(approverId)
.orElseThrow(() -> new UserNotFoundException(approverId));
agreement.setStatus(AgreementStatus.APPROVED);
agreement.setApprovedBy(approver);
agreement.setApprovedAt(Instant.now());
Agreement updated = agreementRepository.save(agreement);
eventProducer.publishAgreementApprovedEvent(updated);
log.info("Agreement approved: id={}", agreementId);
return agreementMapper.toDto(updated);
}
public AgreementDto activateAgreement(Long agreementId) {
log.info("Activating agreement: id={}", agreementId);
Agreement agreement = agreementRepository.findById(agreementId)
.orElseThrow(() -> new AgreementNotFoundException(agreementId));
if (agreement.getStatus() != AgreementStatus.APPROVED) {
throw new IllegalStateException("Only approved agreements can be activated");
}
agreement.setStatus(AgreementStatus.ACTIVE);
Agreement updated = agreementRepository.save(agreement);
eventProducer.publishAgreementActivatedEvent(updated);
return agreementMapper.toDto(updated);
}
public AgreementDto addAmendment(Long agreementId, CreateAmendmentRequest request) {
log.info("Adding amendment to agreement: id={}", agreementId);
Agreement agreement = agreementRepository.findById(agreementId)
.orElseThrow(() -> new AgreementNotFoundException(agreementId));
AgreementAmendment amendment = AgreementAmendment.builder()
.amendmentNumber(agreement.getAmendments().size() + 1)
.description(request.getDescription())
.effectiveDate(request.getEffectiveDate())
.changes(request.getChanges())
.build();
agreement.addAmendment(amendment);
agreement.setStatus(AgreementStatus.AMENDED);
Agreement updated = agreementRepository.save(agreement);
eventProducer.publishAgreementAmendedEvent(updated, amendment);
return agreementMapper.toDto(updated);
}
@Scheduled(cron = "0 0 1 * * *") // Daily at 1 AM
public void processExpiringAgreements() {
log.info("Processing expiring agreements");
LocalDate now = LocalDate.now();
LocalDate notificationThreshold = now.plusDays(90);
List<Agreement> expiringAgreements = agreementRepository
.findByStatusAndEndDateBetween(
AgreementStatus.ACTIVE,
now,
notificationThreshold
);
expiringAgreements.forEach(agreement -> {
if (agreement.isExpiringSoon(90)) {
agreement.setStatus(AgreementStatus.EXPIRING);
agreementRepository.save(agreement);
eventProducer.publishAgreementExpiringEvent(agreement);
}
});
log.info("Processed {} expiring agreements", expiringAgreements.size());
}
private AgreementDto autoApprove(Agreement agreement) {
log.info("Auto-approving agreement: id={}, value={}",
agreement.getId(), agreement.getTotalValue());
agreement.setStatus(AgreementStatus.APPROVED);
agreement.setApprovedAt(Instant.now());
agreement.getMetadata().put("autoApproved", true);
Agreement updated = agreementRepository.save(agreement);
eventProducer.publishAgreementApprovedEvent(updated);
return agreementMapper.toDto(updated);
}
}
Usage Examples
Creating Agreements
What Is Agreement Creation?
Agreement creation establishes a new Master Service Agreement (MSA) between the company and a customer, defining the terms, scope, and pricing for services.
When Is It Used?
- New Customer Onboarding: Creating initial MSA for new customers
- Renewal: Creating new agreement version for renewals
- Service Expansion: Creating additional agreements for new services
- Contract Updates: Creating amended agreements
Creation Flow
Code Example (Java)
// Create agreement with terms
CreateAgreementRequest request = CreateAgreementRequest.builder()
.customerId(1L)
.title("Master Service Agreement - ACME Corp")
.description("Comprehensive MSA covering software licenses and support")
.startDate(LocalDate.now())
.endDate(LocalDate.now().plusYears(3))
.totalValue(new BigDecimal("500000.00"))
.paymentTerms("Net 30")
.termsAndConditions(Map.of(
"autoRenewal", true,
"noticePeriod", "90 days",
"governingLaw", "Delaware",
"services", List.of("Software Licenses", "Technical Support", "Training")
))
.build();
AgreementDto agreement = agreementService.createAgreement(request);
Approval Workflow
What Is the Approval Workflow?
The approval workflow routes agreements through required approval steps based on value, type, and business rules.
When Is It Used?
- Standard Approvals: Agreements requiring manager approval
- High-Value Approvals: Agreements requiring executive approval
- Legal Review: Agreements requiring legal department review
- Multi-Step Approvals: Complex agreements needing multiple approvals
Approval Decision Flow
Code Example (Java)
// Approve agreement
AgreementDto approved = agreementService.approveAgreement(agreementId, approverId);
// Activate approved agreement
AgreementDto activated = agreementService.activateAgreement(agreementId);
Amendment Management
What Is Amendment Management?
Amendment management tracks changes to existing agreements, maintaining a complete history of modifications.
When Is It Used?
- Scope Changes: Adding or removing services
- Pricing Updates: Changing rates or discounts
- Term Extensions: Extending agreement duration
- Terms Modifications: Updating payment terms or conditions
Amendment Process Flow
Code Example (Java)
// Add amendment to agreement
CreateAmendmentRequest amendmentRequest = CreateAmendmentRequest.builder()
.description("Add cloud hosting services")
.effectiveDate(LocalDate.now().plusMonths(1))
.changes(Map.of(
"services", List.of("Cloud Hosting", "Database Management"),
"additionalValue", "50000.00"
))
.build();
AgreementDto amended = agreementService.addAmendment(agreementId, amendmentRequest);
Verification
Agreement Service Tests
What Are Agreement Service Tests?
Agreement service tests verify agreement lifecycle management, approval workflows, amendment tracking, and renewal processing work correctly.
Why Are They Important?
- Workflow Validation: Ensure approval routing works correctly
- Status Transitions: Verify proper state machine behavior
- Amendment Tracking: Confirm changes are recorded accurately
- Renewal Processing: Validate expiration and renewal logic
- Integration: Test coordination with other services
Test Coverage Map
Code Example (Java)
// filepath: /Users/jetstart/dev/jetrev/ink/src/test/java/com/jetrev/ink/service/agreement/AgreementServiceTest.java
@SpringBootTest
@Transactional
class AgreementServiceTest {
@Autowired
private AgreementService agreementService;
@Test
@DisplayName("Should create agreement in draft status")
void shouldCreateAgreementInDraft() {
// Given
CreateAgreementRequest request = CreateAgreementRequest.builder()
.customerId(1L)
.title("Test MSA")
.startDate(LocalDate.now())
.endDate(LocalDate.now().plusYears(1))
.totalValue(new BigDecimal("100000.00"))
.build();
// When
AgreementDto result = agreementService.createAgreement(request);
// Then
assertThat(result.getStatus()).isEqualTo(AgreementStatus.DRAFT);
assertThat(result.getAgreementNumber()).isNotNull();
}
@Test
@DisplayName("Should auto-approve agreements below threshold")
void shouldAutoApproveSmallAgreements() {
// Given
Agreement agreement = createDraftAgreement(new BigDecimal("5000.00"));
// When
AgreementDto result = agreementService.submitForApproval(agreement.getId());
// Then
assertThat(result.getStatus()).isEqualTo(AgreementStatus.APPROVED);
assertThat(result.getApprovedAt()).isNotNull();
assertThat(result.getMetadata().get("autoApproved")).isEqualTo(true);
}
@Test
@DisplayName("Should route large agreements to approval workflow")
void shouldRouteToApprovalWorkflow() {
// Given
Agreement agreement = createDraftAgreement(new BigDecimal("150000.00"));
// When
AgreementDto result = agreementService.submitForApproval(agreement.getId());
// Then
assertThat(result.getStatus()).isEqualTo(AgreementStatus.PENDING_APPROVAL);
verify(approvalWorkflowService).submitForApproval(any());
verify(eventProducer).publishAgreementSubmittedEvent(any());
}
@Test
@DisplayName("Should approve agreement and record approver")
void shouldApproveAgreement() {
// Given
Agreement agreement = createPendingApprovalAgreement();
Long approverId = 10L;
// When
AgreementDto result = agreementService.approveAgreement(
agreement.getId(),
approverId
);
// Then
assertThat(result.getStatus()).isEqualTo(AgreementStatus.APPROVED);
assertThat(result.getApprovedBy()).isEqualTo(approverId);
assertThat(result.getApprovedAt()).isNotNull();
}
@Test
@DisplayName("Should add amendment and update status")
void shouldAddAmendment() {
// Given
Agreement agreement = createActiveAgreement();
CreateAmendmentRequest request = CreateAmendmentRequest.builder()
.description("Add new services")
.effectiveDate(LocalDate.now())
.changes(Map.of("services", List.of("New Service")))
.build();
// When
AgreementDto result = agreementService.addAmendment(
agreement.getId(),
request
);
// Then
Agreement updated = agreementRepository.findById(agreement.getId()).orElseThrow();
assertThat(updated.getStatus()).isEqualTo(AgreementStatus.AMENDED);
assertThat(updated.getAmendments()).hasSize(1);
verify(eventProducer).publishAgreementAmendedEvent(any(), any());
}
@Test
@DisplayName("Should detect expiring agreements")
void shouldDetectExpiringAgreements() {
// Given
Agreement agreement = createActiveAgreement();
agreement.setEndDate(LocalDate.now().plusDays(60));
agreementRepository.save(agreement);
// When
agreementService.processExpiringAgreements();
// Then
Agreement updated = agreementRepository.findById(agreement.getId()).orElseThrow();
assertThat(updated.getStatus()).isEqualTo(AgreementStatus.EXPIRING);
verify(eventProducer).publishAgreementExpiringEvent(any());
}
// Helper methods
private Agreement createDraftAgreement(BigDecimal value) {
return agreementRepository.save(
Agreement.builder()
.customer(createCustomer())
.title("Test Agreement")
.totalValue(value)
.status(AgreementStatus.DRAFT)
.startDate(LocalDate.now())
.endDate(LocalDate.now().plusYears(1))
.build()
);
}
private Agreement createActiveAgreement() {
return agreementRepository.save(
Agreement.builder()
.customer(createCustomer())
.title("Active Agreement")
.totalValue(new BigDecimal("100000.00"))
.status(AgreementStatus.ACTIVE)
.startDate(LocalDate.now())
.endDate(LocalDate.now().plusYears(1))
.build()
);
}
}
Best Practices
- Clear Terms: Always define clear terms and conditions in JSONB
- Approval Tracking: Maintain complete audit trail of all approvals
- Amendment History: Never delete amendments, always preserve history
- Renewal Notifications: Send timely reminders before expiration
- Status Validation: Enforce valid state transitions
- Value Validation: Validate total value against line items
- Date Validation: Ensure end date is after start date
- Document Storage: Link to external document management system
- Customer Validation: Verify customer exists and is active
- Compliance: Follow regulatory requirements for contract management
Related Documentation
Next Steps: Explore Project Management for linking projects to agreements.