← Hub
Implementation Guide
PDI Reference Tickets Architecture Microservice Design
PDI Integration Flow (from PRD) 7 steps · end-to-end
Step 1
Loyalty ID
Phone → PDI
Step 2
Cart Eval
GetRewards
Step 3
Reward Select
Accept/Decline
Step 4
Cart Changes
Re-evaluate
Step 5
Order Place
pending_final
Step 6
Finalize
Deferred
Step 7
Notification
Points summary
Key architectural distinction: FinalizeRewards is deferred until order fulfillment (not at checkout time) to ensure accuracy after substitutions/removals
1

Project Scaffold & Config

Create foundation: NestJS service, Docker setup, environment config

LOY-001 • 10 hours
Week 1 Backend Infrastructure

Key Deliverables

  • NestJS microservice initialized with ConfigModule
  • Docker configuration with PostgreSQL dependency
  • Environment variables documented in .env.example
  • Health check endpoint: GET /loyalty/health
  • Nginx routing configured for /loyalty prefix

Core Files

CREATE

  • modules/loyalty-service/src/main.ts
  • modules/loyalty-service/src/app.module.ts
  • src/health/health.controller.ts
  • Dockerfile
  • .env.example

MODIFY

  • docker-compose.yml (add service)
  • nginx.conf (add route)
  • .env (add variables)

Code Pattern

@Module({
  imports: [ConfigModule.forRoot()],
  controllers: [HealthController],
  providers: [],
})
export class AppModule {}

@Controller('loyalty')
export class HealthController {
  @Get('health')
  health() {
    return { status: 'ok', timestamp: new Date().toISOString() };
  }
}
Tip: Reference existing orders-service structure for consistency. Use same TypeORM setup as other microservices.
2

Database Models & Migrations

Define schema: StoreLoyaltyConfig, LoyaltySession, LoyaltyTransaction entities

LOY-002 • 14 hours
Week 1 Backend Database

Key Deliverables

  • StoreLoyaltyConfig entity with credentials JSONB field
  • LoyaltySession entity with sequence tracking and status enum
  • LoyaltyTransaction entity with order linkage
  • TypeORM entity definitions with proper decorators
  • Versioned migration files executable end-to-end

Core Tables

-- Store Loyalty Configuration
CREATE TABLE store_loyalty_config (
  id SERIAL PRIMARY KEY,
  store_id UUID NOT NULL UNIQUE,
  loyalty_provider VARCHAR(50),
  participant_id VARCHAR(100),
  site_id VARCHAR(100),
  enabled BOOLEAN DEFAULT false,
  credentials JSONB,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Active/Historical Loyalty Sessions
CREATE TABLE loyalty_session (
  id SERIAL PRIMARY KEY,
  session_id UUID NOT NULL UNIQUE,
  order_id UUID,
  store_id UUID NOT NULL,
  loyalty_id VARCHAR(100),
  status VARCHAR(50) DEFAULT 'active',
  rewards JSONB DEFAULT '[]',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Pattern: TypeORM entities with explicit migrations. Naming: YYYYMMDD-description.ts. See database/migrations/ for reference.
3

PDI REST Integration

OAuth2 client credentials, PDI API client, circuit breaker resilience

LOY-003 • 22 hours
Week 1 Backend API Integration

Key Deliverables

  • PdiOAuthService with token caching and TTL refresh
  • PdiRestAdapter for getRewards and finalizeRewards endpoints
  • CircuitBreaker pattern (CLOSED/OPEN/HALF_OPEN states)
  • Exponential backoff retry logic on failures
  • TypeScript type definitions for all PDI contracts

OAuth2 Pattern

@Injectable()
export class PdiOAuthService {
  private tokenCache: { token: string; expiresAt: number } | null = null;

  async getToken(): Promise<string> {
    if (this.tokenCache && this.tokenCache.expiresAt > Date.now()) {
      return this.tokenCache.token;
    }
    const response = await this.requestNewToken();
    this.tokenCache = {
      token: response.access_token,
      expiresAt: Date.now() + (response.expires_in - 300) * 1000
    };
    return response.access_token;
  }
}
Important: Never commit PDI credentials. Use environment variables. Rotate client secrets per environment (qaf, staging, prod).
4

Session State Machine

Session lifecycle: active → pending_finalization → finalized/failed

LOY-004, LOY-005 • 12 hours
Week 1 Backend State Management

Key Deliverables

  • StateTransitionService with 4 valid states
  • Transition guards and validation logic
  • Session initiation endpoint with context
  • Session status query endpoint
  • Rollback mechanism for failed states

State Diagram

enum LoyaltySessionStatus {
  ACTIVE = 'active',
  PENDING_FINALIZATION = 'pending_finalization',
  FINALIZED = 'finalized',
  FAILED = 'failed'
}

Valid Transitions:
active → pending_finalization (checkout initiated)
pending_finalization → finalized (order confirmed)
pending_finalization → failed (error/cancel)
active → failed (cart cleared/error)
5

Medusa Backend Routes

New store API routes, admin endpoints, loyalty middleware

LOY-006, LOY-007 • 16 hours
Week 2 Backend Medusa Integration

Key Deliverables

  • POST /store/loyalty/session - create session
  • GET /store/loyalty/session/:id - session status
  • POST /store/loyalty/rewards - fetch cart rewards
  • GET /admin/loyalty/stores - configured stores
  • POST /admin/loyalty/stores/:id/config - update config
  • Cart/order event subscribers

Route Pattern

router.post('/loyalty/session', async (req, res) => {
  const session = await loyaltyService.createSession({
    storeId: req.store.id,
    customerId: req.customer?.id,
    cartId: req.cart?.id
  });
  res.json(session);
});

router.post('/loyalty/rewards', async (req, res) => {
  const rewards = await loyaltyService.getRewards({
    storeId: req.store.id,
    items: req.body.items,
    sessionId: req.body.sessionId
  });
  res.json(rewards);
});
6

Frontend Components

React components: LoyaltyIdentifier, RewardSelector, PointsDisplay, CheckoutLoyalty

LOY-008 to LOY-012 • 24 hours
Week 2 Frontend React

Key Components

  • LoyaltyIdentifier - phone/email customer lookup
  • RewardSelector - multi-reward selection UI
  • PointsDisplay - customer point balance widget
  • CheckoutLoyalty - checkout flow integration
  • useLoyalty hook - Redux state management

useLoyalty Hook Pattern

export const useLoyalty = () => {
  const dispatch = useDispatch();
  const { session, rewards, loading } = useSelector(s => s.loyalty);

  const initializeSession = async (storeId, customerId) => {
    dispatch(setLoading(true));
    const session = await loyaltyAPI.createSession(storeId, customerId);
    dispatch(setSession(session));
  };

  const selectReward = (rewardId) => {
    dispatch(selectReward(rewardId));
  };

  return { session, rewards, loading, initializeSession, selectReward };
};
Tip: Build with Storybook for isolated testing. Test Redux integration with mock store before E2E tests.
7

Checkout Integration

Finalization flow, order completion hook, error handling and rollback

LOY-013, LOY-014 • 16 hours
Week 2 Backend Integration

Key Deliverables

  • Pre-checkout hook to confirm loyalty eligibility
  • Order completion subscriber to finalize rewards
  • Rollback handler for order cancellation
  • Graceful degradation if PDI unavailable
  • Audit logging for all state changes

Checkout Hook Pattern

// Pre-checkout validation
export const loyaltyPreCheckoutHook = async (cart) => {
  const session = await loyaltyService.getSession(cart.loyaltySessionId);

  if (session.status !== 'active') {
    throw new Error('Loyalty session is not active');
  }

  await loyaltyService.transitionSession(session.id, 'pending_finalization');
  return { valid: true };
};

// Order placement finalization
subscribeToEvent('order.placed', async (order) => {
  const finalized = await loyaltyService.finalizeRewards({
    sessionId: order.loyaltySessionId,
    orderId: order.id,
    items: order.items
  });
  await loyaltyService.transitionSession(order.loyaltySessionId, 'finalized');
});
8

Testing & QA

Unit tests, integration tests, E2E tests, load testing, documentation

LOY-015 to LOY-017 • 32 hours
Week 3 QA Testing

Testing Coverage

  • Unit tests: OAuth, state transitions (>80% coverage)
  • Integration tests: Full session lifecycle, cart coupling
  • E2E tests: Complete customer journey via Cypress/Playwright
  • Load tests: 100+ concurrent sessions, 10k req/min capacity
  • Security tests: Token validation, CORS, input sanitization

Test Pattern - Jest + Supertest

describe('PdiOAuthService', () => {
  it('should cache token and reuse until expiration', async () => {
    const token1 = await service.getToken();
    const token2 = await service.getToken();
    expect(token1).toBe(token2);
    expect(httpService.post).toHaveBeenCalledTimes(1);
  });

  it('should refresh on expiration', async () => {
    jest.useFakeTimers();
    const token1 = await service.getToken();
    jest.advanceTimersByTime(3600 * 1000 + 100);
    const token2 = await service.getToken();
    expect(token1).not.toBe(token2);
  });
});
Tip: Create mock fixtures for PDI responses. Use test database snapshots for reproducible integration tests.