From 333662cb5a2b9d2e330f1204bd19f08d27c45f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Tue, 21 Oct 2025 17:09:06 +0200 Subject: [PATCH 1/9] Init --- .../DirectionsEMEA2025/API_REFERENCE.md | 557 ++++++++++++++++ .../DirectionsEMEA2025.code-workspace | 16 + .../EDocument/DirectionsEMEA2025/README.md | 277 ++++++++ .../DirectionsEMEA2025/WORKSHOP_GUIDE.md | 601 ++++++++++++++++++ .../DirectionsEMEA2025/WORKSHOP_INTRO.md | 487 ++++++++++++++ .../DirectionsEMEA2025/WORKSHOP_PLAN.md | 518 +++++++++++++++ .../DirectionsAuth.Codeunit.al | 122 ++++ .../DirectionsConnectionSetup.Page.al | 138 ++++ .../DirectionsConnectionSetup.Table.al | 94 +++ .../DirectionsIntegration.Codeunit.al | 157 +++++ .../DirectionsIntegration.EnumExt.al | 21 + .../DirectionsRequests.Codeunit.al | 89 +++ .../application/directions_connector/app.json | 32 + .../simple_json/SimpleJsonFormat.Codeunit.al | 240 +++++++ .../simple_json/SimpleJsonFormat.EnumExt.al | 20 + .../simple_json/SimpleJsonHelper.Codeunit.al | 102 +++ .../application/simple_json/app.json | 32 + .../DirectionsEMEA2025/server/README.md | 404 ++++++++++++ .../server/requirements.txt | 1 + .../DirectionsEMEA2025/server/server.py | 51 ++ 20 files changed, 3959 insertions(+) create mode 100644 samples/EDocument/DirectionsEMEA2025/API_REFERENCE.md create mode 100644 samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace create mode 100644 samples/EDocument/DirectionsEMEA2025/README.md create mode 100644 samples/EDocument/DirectionsEMEA2025/WORKSHOP_GUIDE.md create mode 100644 samples/EDocument/DirectionsEMEA2025/WORKSHOP_INTRO.md create mode 100644 samples/EDocument/DirectionsEMEA2025/WORKSHOP_PLAN.md create mode 100644 samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsAuth.Codeunit.al create mode 100644 samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Page.al create mode 100644 samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Table.al create mode 100644 samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.Codeunit.al create mode 100644 samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.EnumExt.al create mode 100644 samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsRequests.Codeunit.al create mode 100644 samples/EDocument/DirectionsEMEA2025/application/directions_connector/app.json create mode 100644 samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al create mode 100644 samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.EnumExt.al create mode 100644 samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonHelper.Codeunit.al create mode 100644 samples/EDocument/DirectionsEMEA2025/application/simple_json/app.json create mode 100644 samples/EDocument/DirectionsEMEA2025/server/README.md create mode 100644 samples/EDocument/DirectionsEMEA2025/server/requirements.txt create mode 100644 samples/EDocument/DirectionsEMEA2025/server/server.py diff --git a/samples/EDocument/DirectionsEMEA2025/API_REFERENCE.md b/samples/EDocument/DirectionsEMEA2025/API_REFERENCE.md new file mode 100644 index 00000000..a154e1a0 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/API_REFERENCE.md @@ -0,0 +1,557 @@ +# Directions Connector API Reference + +Complete API documentation for the workshop server hosted on Azure. + +--- + +## Base URL + +``` +https://[workshop-server].azurewebsites.net/ +``` + +**Note**: The actual URL will be provided by the instructor at the start of the workshop. + +--- + +## Authentication + +All endpoints (except `/register`) require authentication via the `X-Service-Key` header. + +```http +X-Service-Key: your-api-key-here +``` + +The API key is obtained by calling the `/register` endpoint. + +--- + +## Endpoints + +### 1. Register User + +Register a new user and receive an API key. + +**Endpoint**: `POST /register` + +**Authentication**: None + +**Request**: +```http +POST /register HTTP/1.1 +Content-Type: application/json + +{ + "name": "participant-name" +} +``` + +**Response** (200 OK): +```json +{ + "status": "ok", + "key": "12345678-1234-1234-1234-123456789abc" +} +``` + +**Example (cURL)**: +```bash +curl -X POST https://workshop-server.azurewebsites.net/register \ + -H "Content-Type: application/json" \ + -d '{"name": "john-doe"}' +``` + +**Example (PowerShell)**: +```powershell +$body = @{ name = "john-doe" } | ConvertTo-Json +Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/register" ` + -Method Post ` + -Body $body ` + -ContentType "application/json" +``` + +**Notes**: +- Each name should be unique to avoid conflicts +- The API key is returned only once - save it securely +- If you lose your key, you need to register again with a different name + +--- + +### 2. Send Document (Enqueue) + +Add a document to your queue. + +**Endpoint**: `POST /enqueue` + +**Authentication**: Required (`X-Service-Key` header) + +**Request**: +```http +POST /enqueue HTTP/1.1 +X-Service-Key: your-api-key +Content-Type: application/json + +{ + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001", + "customerName": "Contoso Ltd.", + "postingDate": "2025-10-21", + "currencyCode": "USD", + "totalAmount": 1250.00, + "lines": [ + { + "lineNo": 1, + "type": "Item", + "no": "ITEM-001", + "description": "Item A", + "quantity": 5, + "unitPrice": 250.00, + "lineAmount": 1250.00 + } + ] +} +``` + +**Response** (200 OK): +```json +{ + "status": "ok", + "queued_count": 3 +} +``` + +**Example (cURL)**: +```bash +curl -X POST https://workshop-server.azurewebsites.net/enqueue \ + -H "X-Service-Key: your-api-key" \ + -H "Content-Type: application/json" \ + -d '{ + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001", + "customerName": "Contoso Ltd.", + "postingDate": "2025-10-21", + "currencyCode": "USD", + "totalAmount": 1250.00, + "lines": [ + { + "lineNo": 1, + "type": "Item", + "no": "ITEM-001", + "description": "Item A", + "quantity": 5, + "unitPrice": 250.00, + "lineAmount": 1250.00 + } + ] + }' +``` + +**Example (PowerShell)**: +```powershell +$headers = @{ + "X-Service-Key" = "your-api-key" + "Content-Type" = "application/json" +} + +$body = @{ + documentType = "Invoice" + documentNo = "SI-001" + customerNo = "C001" + customerName = "Contoso Ltd." + postingDate = "2025-10-21" + currencyCode = "USD" + totalAmount = 1250.00 + lines = @( + @{ + lineNo = 1 + type = "Item" + no = "ITEM-001" + description = "Item A" + quantity = 5 + unitPrice = 250.00 + lineAmount = 1250.00 + } + ) +} | ConvertTo-Json -Depth 10 + +Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/enqueue" ` + -Method Post ` + -Headers $headers ` + -Body $body +``` + +**Notes**: +- The document is added to your personal queue (isolated by API key) +- You can enqueue any valid JSON document structure +- The `queued_count` returns the total number of documents in your queue + +--- + +### 3. Check Queue (Peek) + +View all documents in your queue without removing them. + +**Endpoint**: `GET /peek` + +**Authentication**: Required (`X-Service-Key` header) + +**Request**: +```http +GET /peek HTTP/1.1 +X-Service-Key: your-api-key +``` + +**Response** (200 OK): +```json +{ + "queued_count": 2, + "items": [ + { + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001", + "customerName": "Contoso Ltd.", + "postingDate": "2025-10-21", + "currencyCode": "USD", + "totalAmount": 1250.00, + "lines": [...] + }, + { + "documentType": "Invoice", + "documentNo": "SI-002", + "customerNo": "C002", + "customerName": "Fabrikam Inc.", + "postingDate": "2025-10-21", + "currencyCode": "EUR", + "totalAmount": 850.00, + "lines": [...] + } + ] +} +``` + +**Example (cURL)**: +```bash +curl -X GET https://workshop-server.azurewebsites.net/peek \ + -H "X-Service-Key: your-api-key" +``` + +**Example (PowerShell)**: +```powershell +$headers = @{ "X-Service-Key" = "your-api-key" } +Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/peek" ` + -Method Get ` + -Headers $headers +``` + +**Example (Browser)**: +You can also use browser extensions like "Modify Header Value" to add the header and view in browser: +``` +https://workshop-server.azurewebsites.net/peek +Header: X-Service-Key: your-api-key +``` + +**Notes**: +- Documents remain in the queue after peeking +- Useful for debugging and verifying document submission +- Returns all documents in your queue (FIFO order) + +--- + +### 4. Retrieve Document (Dequeue) + +Retrieve and remove the first document from your queue. + +**Endpoint**: `GET /dequeue` + +**Authentication**: Required (`X-Service-Key` header) + +**Request**: +```http +GET /dequeue HTTP/1.1 +X-Service-Key: your-api-key +``` + +**Response** (200 OK): +```json +{ + "document": { + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001", + "customerName": "Contoso Ltd.", + "postingDate": "2025-10-21", + "currencyCode": "USD", + "totalAmount": 1250.00, + "lines": [ + { + "lineNo": 1, + "type": "Item", + "no": "ITEM-001", + "description": "Item A", + "quantity": 5, + "unitPrice": 250.00, + "lineAmount": 1250.00 + } + ] + } +} +``` + +**Response** (404 Not Found) - Queue Empty: +```json +{ + "detail": "Queue empty" +} +``` + +**Example (cURL)**: +```bash +curl -X GET https://workshop-server.azurewebsites.net/dequeue \ + -H "X-Service-Key: your-api-key" +``` + +**Example (PowerShell)**: +```powershell +$headers = @{ "X-Service-Key" = "your-api-key" } +Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/dequeue" ` + -Method Get ` + -Headers $headers +``` + +**Notes**: +- Documents are removed from the queue after dequeue (FIFO) +- Returns 404 if the queue is empty +- Once dequeued, the document cannot be retrieved again + +--- + +### 5. Clear Queue + +Clear all documents from your queue. + +**Endpoint**: `DELETE /clear` + +**Authentication**: Required (`X-Service-Key` header) + +**Request**: +```http +DELETE /clear HTTP/1.1 +X-Service-Key: your-api-key +``` + +**Response** (200 OK): +```json +{ + "status": "cleared" +} +``` + +**Example (cURL)**: +```bash +curl -X DELETE https://workshop-server.azurewebsites.net/clear \ + -H "X-Service-Key: your-api-key" +``` + +**Example (PowerShell)**: +```powershell +$headers = @{ "X-Service-Key" = "your-api-key" } +Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/clear" ` + -Method Delete ` + -Headers $headers +``` + +**Notes**: +- Removes all documents from your queue +- Useful for testing and cleanup +- Cannot be undone + +--- + +## Error Responses + +All endpoints may return error responses in the following format: + +### 400 Bad Request +```json +{ + "detail": "Missing name" +} +``` +Returned when required parameters are missing or invalid. + +### 401 Unauthorized +```json +{ + "detail": "Unauthorized or invalid key" +} +``` +Returned when: +- The `X-Service-Key` header is missing +- The API key is invalid +- The API key doesn't match any registered user + +### 404 Not Found +```json +{ + "detail": "Queue empty" +} +``` +Returned when trying to dequeue from an empty queue. + +--- + +## Rate Limits + +Currently, there are **no rate limits** enforced. However, please be considerate of other workshop participants and: +- Don't spam the API with excessive requests +- Use reasonable document sizes (< 1 MB) +- Clean up your queue after testing + +--- + +## Data Persistence + +**Important**: +- All data is stored **in-memory only** +- If the server restarts, all queues and registrations are lost +- Don't rely on this API for production use +- This is a workshop server only + +--- + +## JSON Document Structure + +While you can send any valid JSON, the recommended structure for this workshop is: + +```json +{ + "documentType": "Invoice", // Type of document + "documentNo": "string", // Document number + "customerNo": "string", // Customer/Vendor number + "customerName": "string", // Customer/Vendor name + "postingDate": "YYYY-MM-DD", // ISO date format + "currencyCode": "string", // Currency (USD, EUR, etc.) + "totalAmount": 0.00, // Decimal number + "lines": [ // Array of line items + { + "lineNo": 0, // Integer line number + "type": "string", // Item, G/L Account, etc. + "no": "string", // Item/Account number + "description": "string", // Line description + "quantity": 0.00, // Decimal quantity + "unitPrice": 0.00, // Decimal unit price + "lineAmount": 0.00 // Decimal line amount + } + ] +} +``` + +--- + +## Testing Tips + +### Using Browser Developer Tools + +1. Open browser developer tools (F12) +2. Go to Network tab +3. Call the API endpoints +4. Inspect request/response headers and bodies + +### Using Postman + +1. Import the endpoints into Postman +2. Set up environment variables for base URL and API key +3. Create a collection for easy testing + +### Using VS Code REST Client Extension + +Create a `.http` file: + +```http +### Variables +@baseUrl = https://workshop-server.azurewebsites.net +@apiKey = your-api-key-here + +### Register +POST {{baseUrl}}/register +Content-Type: application/json + +{ + "name": "test-user" +} + +### Enqueue Document +POST {{baseUrl}}/enqueue +X-Service-Key: {{apiKey}} +Content-Type: application/json + +{ + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001" +} + +### Peek Queue +GET {{baseUrl}}/peek +X-Service-Key: {{apiKey}} + +### Dequeue Document +GET {{baseUrl}}/dequeue +X-Service-Key: {{apiKey}} + +### Clear Queue +DELETE {{baseUrl}}/clear +X-Service-Key: {{apiKey}} +``` + +--- + +## Workshop Scenarios + +### Scenario 1: Solo Testing +1. Register with your name +2. Send documents from BC +3. Peek to verify they arrived +4. Dequeue them back into BC + +### Scenario 2: Partner Exchange +1. Each partner registers separately +2. Partner A sends documents +3. Partner B receives and processes them +4. Swap roles + +### Scenario 3: Batch Processing +1. Enqueue multiple documents +2. Peek to see the count +3. Dequeue all at once +4. Process in BC + +--- + +## Support + +If you encounter issues with the API: +1. Check your API key is correct +2. Verify the base URL is accessible +3. Check request headers and body format +4. Look at the error response details +5. Ask the instructor for help + +--- + +## Server Implementation + +The server is built with FastAPI (Python) and is extremely simple: +- In-memory storage (dictionary and deques) +- No database +- No authentication beyond the API key +- No encryption (workshop use only) + +See `server/server.py` for the complete source code. + +--- + +**Happy Testing!** πŸš€ diff --git a/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace b/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace new file mode 100644 index 00000000..8d06a27b --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace @@ -0,0 +1,16 @@ +{ + "folders": [ + { + "path": "." + }, + { + "name": "E-Document Core", + "path": "C:\\depot\\NAV_1\\App\\BCApps\\src\\Apps\\W1\\EDocument\\App" + }, + { + "name": "Avalara Connector", + "path": "C:\\depot\\NAV_1\\App\\BCApps\\src\\Apps\\W1\\EDocumentConnectors\\Avalara\\App" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/samples/EDocument/DirectionsEMEA2025/README.md b/samples/EDocument/DirectionsEMEA2025/README.md new file mode 100644 index 00000000..bbe40796 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/README.md @@ -0,0 +1,277 @@ +# E-Document Connector Workshop +## Directions EMEA 2025 + +Welcome to the E-Document Connector Workshop! This hands-on workshop teaches you how to build complete E-Document integrations using the Business Central E-Document Core framework. + +--- + +## 🎯 Workshop Overview + +**Duration**: 90 minutes (10 min intro + 80 min hands-on) + +**What You'll Build**: +1. **SimpleJson Format** - E-Document format implementation for JSON documents +2. **DirectionsConnector** - HTTP API integration for sending/receiving documents +3. **Complete Round-Trip** - Full document exchange between Business Central and external systems + +**What You'll Learn**: +- How the E-Document Core framework works +- How to implement format interfaces +- How to implement integration interfaces +- Best practices for E-Document solutions + +--- + +## πŸ“‚ Workshop Structure + +``` +DirectionsEMEA2025/ +β”œβ”€β”€ WORKSHOP_INTRO.md # πŸ“Š VS Code presentation (10-minute intro) +β”œβ”€β”€ WORKSHOP_GUIDE.md # πŸ“˜ Step-by-step exercises with solutions +β”œβ”€β”€ API_REFERENCE.md # πŸ”Œ Complete API documentation +β”œβ”€β”€ WORKSHOP_PLAN.md # πŸ“ Detailed implementation plan +β”‚ +β”œβ”€β”€ application/ +β”‚ β”œβ”€β”€ simple_json/ # Exercise 1: Format Extension +β”‚ β”‚ β”œβ”€β”€ app.json +β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al +β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al # ⚠️ TODO sections +β”‚ β”‚ └── SimpleJsonHelper.Codeunit.al # βœ… Pre-written +β”‚ β”‚ +β”‚ └── directions_connector/ # Exercise 2: Integration Extension +β”‚ β”œβ”€β”€ app.json +β”‚ β”œβ”€β”€ DirectionsIntegration.EnumExt.al +β”‚ β”œβ”€β”€ DirectionsIntegration.Codeunit.al # ⚠️ TODO sections +β”‚ β”œβ”€β”€ DirectionsConnectionSetup.Table.al # βœ… Pre-written +β”‚ β”œβ”€β”€ DirectionsConnectionSetup.Page.al # βœ… Pre-written +β”‚ β”œβ”€β”€ DirectionsAuth.Codeunit.al # βœ… Pre-written +β”‚ └── DirectionsRequests.Codeunit.al # βœ… Pre-written +β”‚ +β”œβ”€β”€ solution/ # πŸ“¦ Complete working solution (instructor) +β”‚ β”œβ”€β”€ simple_json/ +β”‚ └── directions_connector/ +β”‚ +└── server/ # 🐍 Python API server (Azure hosted) + β”œβ”€β”€ server.py + β”œβ”€β”€ requirements.txt + └── README.md +``` + +--- + +## πŸš€ Getting Started + +### For Participants + +1. **Read the Introduction** - Open `WORKSHOP_INTRO.md` in VS Code (Ctrl+Shift+V for preview) +2. **Follow the Guide** - Use `WORKSHOP_GUIDE.md` for step-by-step instructions +3. **Reference the API** - Check `API_REFERENCE.md` for endpoint details +4. **Ask Questions** - The instructor is here to help! + +### For Instructors + +1. **Review the Plan** - See `WORKSHOP_PLAN.md` for complete overview +2. **Present the Intro** - Use `WORKSHOP_INTRO.md` as your slide deck in VS Code +3. **Provide API URL** - Update the API Base URL in materials before workshop +4. **Reference Solution** - Complete implementation in `/solution/` folder +5. **Monitor Progress** - Check `/peek` endpoint to see participant submissions + +--- + +## ⏱️ Workshop Timeline + +| Time | Duration | Activity | File | +|------|----------|----------|------| +| 00:00-00:10 | 10 min | Introduction & Architecture | WORKSHOP_INTRO.md | +| 00:10-00:40 | 30 min | Exercise 1: SimpleJson Format | WORKSHOP_GUIDE.md | +| 00:40-01:10 | 30 min | Exercise 2: DirectionsConnector | WORKSHOP_GUIDE.md | +| 01:10-01:25 | 15 min | Testing & Live Demo | WORKSHOP_GUIDE.md | +| 01:25-01:30 | 5 min | Wrap-up & Q&A | - | + +--- + +## πŸ“‹ Prerequisites + +### Required +- Business Central development environment (Sandbox or Docker) +- VS Code with AL Language extension +- Basic AL programming knowledge +- API Base URL (provided by instructor) + +### Helpful +- Understanding of REST APIs +- JSON format familiarity +- Postman or similar API testing tool + +--- + +## πŸŽ“ Learning Objectives + +By the end of this workshop, participants will be able to: + +1. βœ… Understand the E-Document Core framework architecture +2. βœ… Implement the "E-Document" interface for custom formats +3. βœ… Implement IDocumentSender and IDocumentReceiver interfaces +4. βœ… Configure and use E-Document Services in Business Central +5. βœ… Test complete document round-trips (send and receive) +6. βœ… Troubleshoot common integration issues +7. βœ… Know where to find resources for further learning + +--- + +## πŸ“š Key Concepts + +### E-Document Format Interface +Converts Business Central documents to/from external formats: +- **Outgoing**: `Check()`, `Create()`, `CreateBatch()` +- **Incoming**: `GetBasicInfoFromReceivedDocument()`, `GetCompleteInfoFromReceivedDocument()` + +### Integration Interfaces +Communicates with external systems: +- **IDocumentSender**: `Send()` - Send documents +- **IDocumentReceiver**: `ReceiveDocuments()`, `DownloadDocument()` - Receive documents +- **IDocumentResponseHandler**: `GetResponse()` - Async status (advanced) +- **ISentDocumentActions**: Approval/cancellation workflows (advanced) + +### SimpleJson Format +- Simple JSON structure for learning +- Header fields: document type, number, customer, date, amount +- Lines array: line items with quantities and prices +- Human-readable and easy to debug + +### DirectionsConnector +- REST API integration via HTTP +- Authentication via API key header +- Queue-based document exchange +- Stateless and scalable + +--- + +## πŸ§ͺ Testing Scenarios + +### Solo Testing +1. Send documents from your BC instance +2. Verify in API queue via `/peek` +3. Receive documents back into BC +4. Create purchase invoices + +### Partner Testing +1. Partner A sends documents +2. Partner B receives and processes +3. Swap roles and repeat +4. Great for testing interoperability! + +### Group Testing +- Multiple participants send to same queue +- Everyone receives mixed documents +- Tests error handling and validation + +--- + +## πŸ› Troubleshooting + +See the **Troubleshooting** section in `WORKSHOP_GUIDE.md` for: +- Connection issues +- Authentication problems +- JSON parsing errors +- Document creation failures + +Common solutions provided for all scenarios! + +--- + +## πŸ“– Additional Resources + +### Workshop Materials +- [Workshop Introduction](WORKSHOP_INTRO.md) - Slide deck presentation +- [Workshop Guide](WORKSHOP_GUIDE.md) - Detailed instructions with code +- [API Reference](API_REFERENCE.md) - Complete endpoint documentation +- [Workshop Plan](WORKSHOP_PLAN.md) - Implementation overview + +### E-Document Framework +- [E-Document Core README](../../NAV_1/App/BCApps/src/Apps/W1/EDocument/App/README.md) - Official framework documentation +- [E-Document Interface](../../NAV_1/App/BCApps/src/Apps/W1/EDocument/App/src/Document/Interfaces/EDocument.Interface.al) - Interface source code + +### External Resources +- [Business Central Documentation](https://learn.microsoft.com/dynamics365/business-central/) +- [AL Language Reference](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-reference-overview) +- [FastAPI Documentation](https://fastapi.tiangolo.com/) - For the server implementation + +--- + +## 🎁 Bonus Content + +After completing the workshop, try these challenges: + +### Format Enhancements +- Add support for Credit Memos +- Implement batch processing (`CreateBatch()`) +- Add custom field mappings +- Support attachments (PDF, XML) + +### Integration Enhancements +- Implement `IDocumentResponseHandler` for async status +- Add retry logic and error handling +- Implement `ISentDocumentActions` for approvals +- Add custom actions with `IDocumentAction` + +### Real-World Applications +- Connect to actual PEPPOL networks +- Integrate with Avalara or other tax services +- Build EDI integrations +- Create custom XML formats for local requirements + +--- + +## 🀝 Contributing + +This workshop is part of the BCTech repository. If you have suggestions or improvements: + +1. Open an issue on GitHub +2. Submit a pull request +3. Share your feedback with the instructor + +--- + +## πŸ“„ License + +This workshop material is provided under the MIT License. See the repository root for details. + +--- + +## πŸ™ Acknowledgments + +- **Microsoft Business Central Team** - For the E-Document Core framework +- **BCTech Community** - For continuous contributions +- **Workshop Participants** - For your enthusiasm and feedback! + +--- + +## πŸ“ž Support + +### During the Workshop +- Ask the instructor +- Check the WORKSHOP_GUIDE.md +- Collaborate with neighbors + +### After the Workshop +- GitHub Issues: [BCTech Repository](https://github.com/microsoft/BCTech) +- Documentation: [Learn Microsoft](https://learn.microsoft.com/dynamics365/business-central/) +- Community: [Business Central Forums](https://community.dynamics.com/forums/thread/) + +--- + +## 🎯 Quick Start + +**Ready to begin? Follow these steps:** + +1. βœ… Open `WORKSHOP_INTRO.md` and read through the introduction +2. βœ… Get the API Base URL from your instructor +3. βœ… Open `WORKSHOP_GUIDE.md` and start Exercise 1 +4. βœ… Have fun building your E-Document integration! + +--- + +**Happy Coding!** πŸš€ + +*For questions or feedback, contact the workshop instructor.* diff --git a/samples/EDocument/DirectionsEMEA2025/WORKSHOP_GUIDE.md b/samples/EDocument/DirectionsEMEA2025/WORKSHOP_GUIDE.md new file mode 100644 index 00000000..cbcd304e --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/WORKSHOP_GUIDE.md @@ -0,0 +1,601 @@ +# Directions EMEA 2025 - E-Document Connector Workshop + +## Workshop Guide + +Welcome to the E-Document Connector Workshop! In this hands-on session, you'll build a complete E-Document solution that integrates Business Central with an external API using the E-Document Core framework. + +--- + +## 🎯 What You'll Build + +By the end of this workshop, you will have: +1. **SimpleJson Format** - Convert Sales Invoices to JSON and parse incoming Purchase Invoices +2. **DirectionsConnector** - Send and receive documents via HTTP API +3. **Complete Integration** - Full round-trip document exchange + +--- + +## ⏱️ Timeline + +- **Exercise 1** (30 min): Implement SimpleJson Format +- **Exercise 2** (30 min): Implement DirectionsConnector +- **Testing** (15 min): End-to-end validation + +--- + +## πŸ“‹ Prerequisites + +### Required +- Business Central environment (Sandbox or Docker) +- VS Code with AL Language extension +- API Base URL: `[Provided by instructor]` + +### Workshop Files +Your workspace contains: +``` +application/ + β”œβ”€β”€ simple_json/ # Exercise 1 + └── directions_connector/ # Exercise 2 +``` + +--- + +## πŸš€ Exercise 1: SimpleJson Format (30 minutes) + +In this exercise, you'll implement the **"E-Document" interface** to convert Business Central documents to/from JSON format. + +### Part A: Validate Outgoing Documents (5 minutes) + +**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` + +**Find**: The `Check()` procedure (around line 27) + +**Task**: Add validation to ensure required fields are filled before creating the document. + +**Implementation**: +```al +procedure Check(var SourceDocumentHeader: RecordRef; EDocumentService: Record "E-Document Service"; EDocumentProcessingPhase: Enum "E-Document Processing Phase") +var + SalesInvoiceHeader: Record "Sales Invoice Header"; +begin + case SourceDocumentHeader.Number of + Database::"Sales Invoice Header": + begin + // Validate Customer No. is filled + SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Sell-to Customer No.")).TestField(); + + // Validate Posting Date is filled + SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Posting Date")).TestField(); + end; + end; +end; +``` + +**βœ… Validation**: Try posting a Sales Invoice - it should validate required fields. + +--- + +### Part B: Create JSON from Sales Invoice (15 minutes) + +**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` + +**Find**: The `CreateSalesInvoiceJson()` procedure (around line 93) + +**Task**: Generate JSON representation of a Sales Invoice with header and lines. + +**Implementation**: +```al +local procedure CreateSalesInvoiceJson(var SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; var OutStr: OutStream) +var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + RootObject: JsonObject; + LinesArray: JsonArray; + LineObject: JsonObject; + JsonText: Text; +begin + // Get the actual records from RecordRef + SourceDocumentHeader.SetTable(SalesInvoiceHeader); + SourceDocumentLines.SetTable(SalesInvoiceLine); + + // Add header fields to JSON object + RootObject.Add('documentType', 'Invoice'); + RootObject.Add('documentNo', SalesInvoiceHeader."No."); + RootObject.Add('customerNo', SalesInvoiceHeader."Sell-to Customer No."); + RootObject.Add('customerName', SalesInvoiceHeader."Sell-to Customer Name"); + RootObject.Add('postingDate', Format(SalesInvoiceHeader."Posting Date", 0, '--')); + RootObject.Add('currencyCode', SalesInvoiceHeader."Currency Code"); + RootObject.Add('totalAmount', SalesInvoiceHeader."Amount Including VAT"); + + // Create lines array + if SalesInvoiceLine.FindSet() then + repeat + // Create line object + Clear(LineObject); + LineObject.Add('lineNo', SalesInvoiceLine."Line No."); + LineObject.Add('type', Format(SalesInvoiceLine.Type)); + LineObject.Add('no', SalesInvoiceLine."No."); + LineObject.Add('description', SalesInvoiceLine.Description); + LineObject.Add('quantity', SalesInvoiceLine.Quantity); + LineObject.Add('unitPrice', SalesInvoiceLine."Unit Price"); + LineObject.Add('lineAmount', SalesInvoiceLine."Amount Including VAT"); + LinesArray.Add(LineObject); + until SalesInvoiceLine.Next() = 0; + + // Add lines array to root object + RootObject.Add('lines', LinesArray); + + // Write JSON to stream + RootObject.WriteTo(JsonText); + OutStr.WriteText(JsonText); +end; +``` + +**βœ… Validation**: +1. Create and post a Sales Invoice +2. Open the E-Document list +3. View the E-Document and check the JSON content in the log + +--- + +### Part C: Parse Incoming JSON (Basic Info) (5 minutes) + +**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` + +**Find**: The `GetBasicInfoFromReceivedDocument()` procedure (around line 151) + +**Task**: Extract basic information from incoming JSON to populate E-Document fields. + +**Implementation**: +```al +procedure GetBasicInfoFromReceivedDocument(var EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob") +var + JsonObject: JsonObject; + JsonToken: JsonToken; + SimpleJsonHelper: Codeunit "SimpleJson Helper"; +begin + // Parse JSON from blob + if not SimpleJsonHelper.ReadJsonFromBlob(TempBlob, JsonObject) then + Error('Failed to parse JSON document'); + + // Set document type to Purchase Invoice + EDocument."Document Type" := EDocument."Document Type"::"Purchase Invoice"; + + // Extract document number + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'documentNo', JsonToken) then + EDocument."Document No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Document No.")); + + // Extract vendor number (from customerNo in JSON) + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerNo', JsonToken) then + EDocument."Bill-to/Pay-to No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Bill-to/Pay-to No.")); + + // Extract vendor name + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerName', JsonToken) then + EDocument."Bill-to/Pay-to Name" := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Bill-to/Pay-to Name")); + + // Extract posting date + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then + EDocument."Document Date" := SimpleJsonHelper.GetJsonTokenDate(JsonToken); + + // Extract currency code + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'currencyCode', JsonToken) then + EDocument."Currency Code" := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Currency Code")); +end; +``` + +**βœ… Validation**: This will be tested in Exercise 2 when receiving documents. + +--- + +### Part D: Create Purchase Invoice from JSON (5 minutes) + +**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` + +**Find**: The `GetCompleteInfoFromReceivedDocument()` procedure (around line 188) + +**Task**: Create a Purchase Invoice record from JSON data. + +**Implementation**: +```al +procedure GetCompleteInfoFromReceivedDocument(var EDocument: Record "E-Document"; var CreatedDocumentHeader: RecordRef; var CreatedDocumentLines: RecordRef; var TempBlob: Codeunit "Temp Blob") +var + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + JsonObject: JsonObject; + JsonToken: JsonToken; + JsonArray: JsonArray; + JsonLineToken: JsonToken; + SimpleJsonHelper: Codeunit "SimpleJson Helper"; + LineNo: Integer; +begin + // Parse JSON from blob + if not SimpleJsonHelper.ReadJsonFromBlob(TempBlob, JsonObject) then + Error('Failed to parse JSON document'); + + // Create Purchase Header + PurchaseHeader.Init(); + PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::Invoice; + PurchaseHeader.Insert(true); + + // Set vendor from JSON + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerNo', JsonToken) then + PurchaseHeader.Validate("Buy-from Vendor No.", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); + + // Set posting date + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then + PurchaseHeader.Validate("Posting Date", SimpleJsonHelper.GetJsonTokenDate(JsonToken)); + + // Set currency code (if not blank) + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'currencyCode', JsonToken) then begin + if SimpleJsonHelper.GetJsonTokenValue(JsonToken) <> '' then + PurchaseHeader.Validate("Currency Code", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); + end; + + PurchaseHeader.Modify(true); + + // Create Purchase Lines from JSON array + if JsonObject.Get('lines', JsonToken) then begin + JsonArray := JsonToken.AsArray(); + LineNo := 10000; + + foreach JsonLineToken in JsonArray do begin + JsonObject := JsonLineToken.AsObject(); + + PurchaseLine.Init(); + PurchaseLine."Document Type" := PurchaseHeader."Document Type"; + PurchaseLine."Document No." := PurchaseHeader."No."; + PurchaseLine."Line No." := LineNo; + + // Set type (default to Item) + PurchaseLine.Type := PurchaseLine.Type::Item; + + // Set item number + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'no', JsonToken) then + PurchaseLine.Validate("No.", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); + + // Set description + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'description', JsonToken) then + PurchaseLine.Description := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(PurchaseLine.Description)); + + // Set quantity + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'quantity', JsonToken) then + PurchaseLine.Validate(Quantity, SimpleJsonHelper.GetJsonTokenDecimal(JsonToken)); + + // Set unit price + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'unitPrice', JsonToken) then + PurchaseLine.Validate("Direct Unit Cost", SimpleJsonHelper.GetJsonTokenDecimal(JsonToken)); + + PurchaseLine.Insert(true); + LineNo += 10000; + end; + end; + + // Return via RecordRef + CreatedDocumentHeader.GetTable(PurchaseHeader); + CreatedDocumentLines.GetTable(PurchaseLine); +end; +``` + +**βœ… Validation**: This will be tested in Exercise 2 when creating documents from received E-Documents. + +--- + +## πŸ”Œ Exercise 2: DirectionsConnector (30 minutes) + +In this exercise, you'll implement the **IDocumentSender** and **IDocumentReceiver** interfaces to send/receive documents via HTTP API. + +### Part A: Setup Connection (5 minutes) + +**Manual Setup**: +1. Open Business Central +2. Search for "Directions Connection Setup" +3. Enter the API Base URL: `[Provided by instructor]` +4. Enter your name (unique identifier) +5. Click "Register" to get your API key +6. Click "Test Connection" to verify + +**βœ… Validation**: You should see "Connection test successful!" + +--- + +### Part B: Send Document (10 minutes) + +**File**: `application/directions_connector/DirectionsIntegration.Codeunit.al` + +**Find**: The `Send()` procedure (around line 31) + +**Task**: Send an E-Document to the API /enqueue endpoint. + +**Implementation**: +```al +procedure Send(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; SendContext: Codeunit SendContext) +var + DirectionsSetup: Record "Directions Connection Setup"; + DirectionsAuth: Codeunit "Directions Auth"; + DirectionsRequests: Codeunit "Directions Requests"; + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + TempBlob: Codeunit "Temp Blob"; + JsonContent: Text; +begin + // Get connection setup + DirectionsAuth.GetConnectionSetup(DirectionsSetup); + + // Get document content from SendContext + SendContext.GetTempBlob(TempBlob); + JsonContent := DirectionsRequests.ReadJsonFromBlob(TempBlob); + + // Create POST request to /enqueue + DirectionsRequests.CreatePostRequest(DirectionsSetup."API Base URL" + 'enqueue', JsonContent, HttpRequest); + + // Add authentication header + DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); + + // Log request (for E-Document framework logging) + SendContext.Http().SetHttpRequestMessage(HttpRequest); + + // Send request + if not HttpClient.Send(HttpRequest, HttpResponse) then + Error('Failed to send document to API.'); + + // Log response and check success + SendContext.Http().SetHttpResponseMessage(HttpResponse); + DirectionsRequests.CheckResponseSuccess(HttpResponse); +end; +``` + +**βœ… Validation**: +1. Create and post a Sales Invoice +2. Open the E-Document list +3. Send the E-Document (it should succeed) +4. Use the /peek endpoint in a browser to verify the document is in the queue + +--- + +### Part C: Receive Documents List (10 minutes) + +**File**: `application/directions_connector/DirectionsIntegration.Codeunit.al` + +**Find**: The `ReceiveDocuments()` procedure (around line 72) + +**Task**: Retrieve the list of available documents from the API /peek endpoint. + +**Implementation**: +```al +procedure ReceiveDocuments(var EDocumentService: Record "E-Document Service"; DocumentsMetadata: Codeunit "Temp Blob List"; ReceiveContext: Codeunit ReceiveContext) +var + DirectionsSetup: Record "Directions Connection Setup"; + DirectionsAuth: Codeunit "Directions Auth"; + DirectionsRequests: Codeunit "Directions Requests"; + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + JsonObject: JsonObject; + JsonToken: JsonToken; + JsonArray: JsonArray; + TempBlob: Codeunit "Temp Blob"; + ResponseText: Text; + DocumentJson: Text; +begin + // Get connection setup + DirectionsAuth.GetConnectionSetup(DirectionsSetup); + + // Create GET request to /peek + DirectionsRequests.CreateGetRequest(DirectionsSetup."API Base URL" + 'peek', HttpRequest); + + // Add authentication + DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); + + // Log request + ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); + + // Send request + if not HttpClient.Send(HttpRequest, HttpResponse) then + Error('Failed to retrieve documents from API.'); + + // Log response and check success + ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); + DirectionsRequests.CheckResponseSuccess(HttpResponse); + + // Parse response and extract documents + ResponseText := DirectionsRequests.GetResponseText(HttpResponse); + if JsonObject.ReadFrom(ResponseText) then begin + if JsonObject.Get('items', JsonToken) then begin + JsonArray := JsonToken.AsArray(); + foreach JsonToken in JsonArray do begin + // Create a TempBlob for each document metadata + Clear(TempBlob); + JsonToken.WriteTo(DocumentJson); + DirectionsRequests.WriteTextToBlob(DocumentJson, TempBlob); + DocumentsMetadata.Add(TempBlob); + end; + end; + end; +end; +``` + +**βœ… Validation**: This will be tested when running "Get Documents" action in BC. + +--- + +### Part D: Download Single Document (5 minutes) + +**File**: `application/directions_connector/DirectionsIntegration.Codeunit.al` + +**Find**: The `DownloadDocument()` procedure (around line 135) + +**Task**: Download a single document from the API /dequeue endpoint. + +**Implementation**: +```al +procedure DownloadDocument(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; DocumentMetadata: codeunit "Temp Blob"; ReceiveContext: Codeunit ReceiveContext) +var + DirectionsSetup: Record "Directions Connection Setup"; + DirectionsAuth: Codeunit "Directions Auth"; + DirectionsRequests: Codeunit "Directions Requests"; + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + JsonObject: JsonObject; + JsonToken: JsonToken; + TempBlob: Codeunit "Temp Blob"; + ResponseText: Text; + DocumentJson: Text; +begin + // Get connection setup + DirectionsAuth.GetConnectionSetup(DirectionsSetup); + + // Create GET request to /dequeue + DirectionsRequests.CreateGetRequest(DirectionsSetup."API Base URL" + 'dequeue', HttpRequest); + + // Add authentication + DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); + + // Log request + ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); + + // Send request + if not HttpClient.Send(HttpRequest, HttpResponse) then + Error('Failed to download document from API.'); + + // Log response and check success + ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); + DirectionsRequests.CheckResponseSuccess(HttpResponse); + + // Parse response and extract document + ResponseText := DirectionsRequests.GetResponseText(HttpResponse); + if JsonObject.ReadFrom(ResponseText) then begin + if JsonObject.Get('document', JsonToken) then begin + JsonToken.WriteTo(DocumentJson); + ReceiveContext.GetTempBlob(TempBlob); + DirectionsRequests.WriteTextToBlob(DocumentJson, TempBlob); + end else + Error('No document found in response.'); + end; +end; +``` + +**βœ… Validation**: This will be tested in the complete flow below. + +--- + +## πŸ§ͺ Testing - Complete Flow (15 minutes) + +### Setup E-Document Service + +1. Open "E-Document Services" +2. Create a new service: + - **Code**: DIRECTIONS + - **Description**: Directions Connector + - **Document Format**: Simple JSON Format + - **Service Integration**: Directions Connector +3. Click "Setup Service Integration" and verify your connection +4. Enable the service + +### Test Outgoing Flow + +1. **Create Sales Invoice**: + - Customer: Any customer + - Add at least one line item + - Post the invoice + +2. **Send E-Document**: + - Open "E-Documents" list + - Find your posted invoice + - Action: "Send" + - Status should change to "Sent" + +3. **Verify in API**: + - Open browser: `[API Base URL]/peek` + - Add header: `X-Service-Key: [Your API Key]` + - You should see your document in the "items" array + +### Test Incoming Flow + +1. **Receive Documents**: + - Open "E-Document Services" + - Select your DIRECTIONS service + - Action: "Get Documents" + - New E-Documents should appear with status "Imported" + +2. **View Received Document**: + - Open the received E-Document + - Check the JSON content in the log + - Verify basic info is populated (vendor, date, etc.) + +3. **Create Purchase Invoice**: + - Action: "Create Document" + - A Purchase Invoice should be created + - Open the Purchase Invoice and verify: + - Vendor is set correctly + - Lines are populated with items, quantities, prices + - Dates and currency match + +4. **Verify Queue**: + - Check /peek endpoint again + - The queue should be empty (documents were dequeued) + +--- + +## πŸŽ‰ Success Criteria + +You have successfully completed the workshop if: +- βœ… You can post a Sales Invoice and see it as an E-Document +- βœ… The E-Document contains valid JSON +- βœ… You can send the E-Document to the API +- βœ… The document appears in the API queue +- βœ… You can receive documents from the API +- βœ… Purchase Invoices are created from received documents +- βœ… All data is mapped correctly + +--- + +## πŸ› Troubleshooting + +### "Failed to connect to API" +- Check the API Base URL in setup +- Verify the API server is running +- Check firewall settings + +### "Unauthorized or invalid key" +- Re-register in the setup page +- Verify the API key is saved +- Check that you're using the correct key header + +### "Document type not supported" +- Verify you selected "Simple JSON Format" in E-Document Service +- Check the format enum extension is compiled + +### "Failed to parse JSON" +- Check the JSON structure in E-Document log +- Verify all required fields are present +- Look for syntax errors (missing commas, brackets) + +### "Vendor does not exist" +- Create a vendor with the same number as the customer in the JSON +- Or modify the JSON to use an existing vendor number + +--- + +## πŸ“š Additional Resources + +- [E-Document Core README](../../README.md) +- [API Reference](../API_REFERENCE.md) +- [Workshop Plan](../WORKSHOP_PLAN.md) + +--- + +## πŸŽ“ Homework / Advanced Exercises + +Want to learn more? Try these: +1. Add support for Credit Memos +2. Implement `GetResponse()` for async status checking +3. Add custom fields to the JSON format +4. Implement validation rules for incoming documents +5. Add error handling and retry logic +6. Create a batch send function + +--- + +**Congratulations!** 🎊 You've completed the E-Document Connector Workshop! diff --git a/samples/EDocument/DirectionsEMEA2025/WORKSHOP_INTRO.md b/samples/EDocument/DirectionsEMEA2025/WORKSHOP_INTRO.md new file mode 100644 index 00000000..79fc7697 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/WORKSHOP_INTRO.md @@ -0,0 +1,487 @@ +# 🎯 E-Document Connector Workshop +## Directions EMEA 2025 + +--- + +## πŸ‘‹ Welcome! + +In the next **90 minutes**, you'll build a complete E-Document integration solution. + +**What you'll learn:** +- How the E-Document Core framework works +- How to implement format interfaces (JSON) +- How to implement integration interfaces (HTTP API) +- Complete round-trip document exchange + +**What you'll build:** +- βœ… SimpleJson Format - Convert documents to/from JSON +- βœ… DirectionsConnector - Send/receive via HTTP API + +--- + +## πŸ—οΈ Architecture Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Business Central β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Sales Invoice│───▢│ E-Document Core β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Format Interface│◀─── You implement! β”‚ +β”‚ β”‚ (SimpleJson) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ JSON Blob β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Integration Interface │◀─── You implement! β”‚ +β”‚ β”‚ (DirectionsConnector) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ HTTPS + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Azure API Server β”‚ + β”‚ β”‚ + β”‚ Queue Management β”‚ + β”‚ Document Storage β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Another Company / β”‚ + β”‚ Trading Partner β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## πŸ“¦ E-Document Core Framework + +The framework provides: + +### 1️⃣ **Document Interface** (`"E-Document"`) +Convert documents to/from Business Central + +**Outgoing** (BC β†’ External): +- `Check()` - Validate before sending +- `Create()` - Convert to format (JSON, XML, etc.) +- `CreateBatch()` - Batch multiple documents + +**Incoming** (External β†’ BC): +- `GetBasicInfoFromReceivedDocument()` - Parse metadata +- `GetCompleteInfoFromReceivedDocument()` - Create BC document + +### 2️⃣ **Integration Interfaces** +Send/receive documents via various channels + +**IDocumentSender**: +- `Send()` - Send document to external service + +**IDocumentReceiver**: +- `ReceiveDocuments()` - Get list of available documents +- `DownloadDocument()` - Download specific document + +**Others** (Advanced): +- `IDocumentResponseHandler` - Async status checking +- `ISentDocumentActions` - Approval/cancellation +- `IDocumentAction` - Custom actions + +--- + +## 🎨 What is SimpleJson? + +A simple JSON format for E-Documents designed for this workshop. + +### Example JSON Structure: + +```json +{ + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001", + "customerName": "Contoso Ltd.", + "postingDate": "2025-10-21", + "currencyCode": "USD", + "totalAmount": 1250.00, + "lines": [ + { + "lineNo": 1, + "type": "Item", + "no": "ITEM-001", + "description": "Item A", + "quantity": 5, + "unitPrice": 250.00, + "lineAmount": 1250.00 + } + ] +} +``` + +**Why JSON?** +- βœ… Human-readable +- βœ… Easy to parse +- βœ… Widely supported +- βœ… Perfect for learning + +--- + +## πŸ”Œ What is DirectionsConnector? + +An HTTP-based integration that sends/receives documents via REST API. + +### API Endpoints: + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/register` | POST | Get API key | +| `/enqueue` | POST | Send document | +| `/peek` | GET | View queue | +| `/dequeue` | GET | Receive document | +| `/clear` | DELETE | Clear queue | + +### Authentication: +```http +X-Service-Key: your-api-key-here +``` + +### Why HTTP API? +- βœ… Simple and universal +- βœ… Easy to test (browser, Postman) +- βœ… Real-world scenario +- βœ… Stateless and scalable + +--- + +## πŸ”„ Complete Flow + +### Outgoing (Sending): +``` +Sales Invoice (BC) + ↓ Post +E-Document Created + ↓ Format: SimpleJson.Create() +JSON Blob + ↓ Integration: DirectionsConnector.Send() +HTTP POST /enqueue + ↓ +Azure API Queue +``` + +### Incoming (Receiving): +``` +Azure API Queue + ↓ Integration: DirectionsConnector.ReceiveDocuments() +HTTP GET /peek (list) + ↓ Integration: DirectionsConnector.DownloadDocument() +HTTP GET /dequeue (download) + ↓ +JSON Blob + ↓ Format: SimpleJson.GetBasicInfo() +E-Document Created (Imported) + ↓ Format: SimpleJson.GetCompleteInfo() +Purchase Invoice (BC) +``` + +--- + +## ⏱️ Workshop Timeline + +| Time | Duration | Activity | +|------|----------|----------| +| 00:00 | 10 min | ← You are here! (Introduction) | +| 00:10 | 30 min | **Exercise 1**: Implement SimpleJson Format | +| 00:40 | 30 min | **Exercise 2**: Implement DirectionsConnector | +| 01:10 | 15 min | Testing & Live Demo | +| 01:25 | 5 min | Wrap-up & Q&A | + +--- + +## πŸ“ Exercise 1: SimpleJson Format (30 min) + +Implement the **"E-Document" interface** + +### Part A: Check() - 5 minutes +Validate required fields before creating document +```al +procedure Check(var SourceDocumentHeader: RecordRef; ...) +begin + // TODO: Validate Customer No. + // TODO: Validate Posting Date +end; +``` + +### Part B: Create() - 15 minutes +Convert Sales Invoice to JSON +```al +procedure Create(...; var TempBlob: Codeunit "Temp Blob") +begin + // TODO: Generate JSON from Sales Invoice + // TODO: Include header and lines +end; +``` + +### Part C: GetBasicInfo() - 5 minutes +Parse incoming JSON metadata +```al +procedure GetBasicInfoFromReceivedDocument(var EDocument: Record "E-Document"; ...) +begin + // TODO: Parse JSON + // TODO: Set document type, number, date +end; +``` + +### Part D: GetCompleteInfo() - 5 minutes +Create Purchase Invoice from JSON +```al +procedure GetCompleteInfoFromReceivedDocument(...) +begin + // TODO: Parse JSON + // TODO: Create Purchase Header & Lines +end; +``` + +--- + +## πŸ”Œ Exercise 2: DirectionsConnector (30 min) + +Implement **IDocumentSender** and **IDocumentReceiver** interfaces + +### Part A: Setup - 5 minutes +Configure connection in BC +- API Base URL +- Register to get API Key +- Test connection + +### Part B: Send() - 10 minutes +Send document to API +```al +procedure Send(var EDocument: Record "E-Document"; ...) +begin + // TODO: Get JSON from SendContext + // TODO: POST to /enqueue endpoint + // TODO: Handle response +end; +``` + +### Part C: ReceiveDocuments() - 10 minutes +Get list of available documents +```al +procedure ReceiveDocuments(...; DocumentsMetadata: Codeunit "Temp Blob List"; ...) +begin + // TODO: GET from /peek endpoint + // TODO: Parse items array + // TODO: Add each to DocumentsMetadata list +end; +``` + +### Part D: DownloadDocument() - 5 minutes +Download specific document +```al +procedure DownloadDocument(var EDocument: Record "E-Document"; ...) +begin + // TODO: GET from /dequeue endpoint + // TODO: Parse response + // TODO: Store in TempBlob +end; +``` + +--- + +## 🎯 Success Criteria + +By the end, you should be able to: + +βœ… **Create** a Sales Invoice in BC +βœ… **Convert** it to JSON via SimpleJson format +βœ… **Send** it to Azure API via DirectionsConnector +βœ… **Verify** it appears in the queue +βœ… **Receive** documents from the API +βœ… **Parse** JSON and extract metadata +βœ… **Create** Purchase Invoices from received documents + +**You'll have built a complete E-Document integration!** πŸŽ‰ + +--- + +## πŸ› οΈ What's Pre-Written? + +To save time, these are already implemented: + +### SimpleJson Format: +- βœ… Extension setup (app.json) +- βœ… Enum extension +- βœ… Helper methods for JSON operations +- βœ… Error handling framework + +### DirectionsConnector: +- βœ… Connection Setup table & page +- βœ… Authentication helpers +- βœ… HTTP request builders +- βœ… Registration logic + +**You focus on the business logic!** + +--- + +## πŸ“‚ Your Workspace + +``` +application/ + β”œβ”€β”€ simple_json/ + β”‚ β”œβ”€β”€ app.json βœ… Pre-written + β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al βœ… Pre-written + β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al ⚠️ TODO sections + β”‚ └── SimpleJsonHelper.Codeunit.al βœ… Pre-written + β”‚ + └── directions_connector/ + β”œβ”€β”€ app.json βœ… Pre-written + β”œβ”€β”€ DirectionsIntegration.EnumExt.al βœ… Pre-written + β”œβ”€β”€ DirectionsIntegration.Codeunit.al ⚠️ TODO sections + β”œβ”€β”€ DirectionsConnectionSetup.Table.al βœ… Pre-written + β”œβ”€β”€ DirectionsConnectionSetup.Page.al βœ… Pre-written + β”œβ”€β”€ DirectionsAuth.Codeunit.al βœ… Pre-written + └── DirectionsRequests.Codeunit.al βœ… Pre-written +``` + +--- + +## πŸ“š Resources Available + +During the workshop: + +1. **WORKSHOP_GUIDE.md** - Step-by-step instructions with full code +2. **API_REFERENCE.md** - Complete API documentation +3. **README.md** (E-Document Core) - Framework reference +4. **Instructor** - Available for questions! + +After the workshop: +- Complete solution in `/solution/` folder +- Homework exercises +- Additional resources + +--- + +## πŸ’‘ Tips for Success + +1. **Read the TODO comments** - They contain hints and instructions +2. **Use the helper methods** - They're pre-written to save time +3. **Test incrementally** - Don't wait until the end +4. **Check the logs** - E-Document framework logs everything +5. **Ask questions** - The instructor is here to help! +6. **Have fun!** - This is a hands-on learning experience + +--- + +## πŸ› Common Pitfalls + +Watch out for: +- ❌ Missing commas in JSON +- ❌ Forgetting to add authentication headers +- ❌ Not handling empty lines/arrays +- ❌ Incorrect RecordRef table numbers +- ❌ Not logging HTTP requests/responses + +**The workshop guide has solutions for all of these!** + +--- + +## πŸŽ“ Beyond the Workshop + +After mastering the basics, explore: + +**Advanced Format Features:** +- Support multiple document types +- Add custom field mappings +- Implement validation rules +- Handle attachments (PDF, XML) + +**Advanced Integration Features:** +- Async status checking (`IDocumentResponseHandler`) +- Approval workflows (`ISentDocumentActions`) +- Batch processing +- Error handling and retry logic +- Custom actions (`IDocumentAction`) + +**Real-World Scenarios:** +- PEPPOL format +- Avalara integration +- Custom XML formats +- EDI integrations + +--- + +## 🀝 Workshop Collaboration + +### Partner Up! +- Work with a neighbor +- Share your API key to exchange documents +- Test each other's implementations + +### Group Testing +- Send documents to the group queue +- Everyone receives and processes them +- Great way to test at scale! + +--- + +## πŸš€ Let's Get Started! + +1. **Open** `WORKSHOP_GUIDE.md` for step-by-step instructions +2. **Navigate** to `application/simple_json/SimpleJsonFormat.Codeunit.al` +3. **Find** the first TODO section in the `Check()` method +4. **Start coding!** + +**Timer starts... NOW!** ⏰ + +--- + +## ❓ Questions? + +Before we start: +- ❓ Is everyone able to access the API URL? +- ❓ Does everyone have their development environment ready? +- ❓ Any questions about the architecture or flow? + +--- + +## πŸ“ž Need Help? + +During the workshop: +- πŸ™‹ Raise your hand +- πŸ’¬ Ask your neighbor +- πŸ“– Check the WORKSHOP_GUIDE.md +- πŸ” Look at the API_REFERENCE.md +- πŸ‘¨β€πŸ« Ask the instructor + +**We're all here to learn together!** + +--- + +# πŸŽ‰ Good Luck! + +**Remember**: The goal is to learn, not to finish first. +Take your time, experiment, and enjoy the process! + +**Now let's build something awesome!** πŸ’ͺ + +--- + +## Quick Links + +- πŸ“˜ [Workshop Guide](./WORKSHOP_GUIDE.md) - Step-by-step instructions +- πŸ”Œ [API Reference](./API_REFERENCE.md) - Endpoint documentation +- πŸ“ [Workshop Plan](./WORKSHOP_PLAN.md) - Overview and structure +- πŸ“š [E-Document Core README](../../../NAV_1/App/BCApps/src/Apps/W1/EDocument/App/README.md) - Framework docs + +**API Base URL**: `[Will be provided by instructor]` + +--- + +*Press `Ctrl+Shift+V` in VS Code to view this file in preview mode with formatting!* diff --git a/samples/EDocument/DirectionsEMEA2025/WORKSHOP_PLAN.md b/samples/EDocument/DirectionsEMEA2025/WORKSHOP_PLAN.md new file mode 100644 index 00000000..b40d428a --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/WORKSHOP_PLAN.md @@ -0,0 +1,518 @@ +# E-Document Connector Workshop - Implementation Plan + +## Workshop Overview +**Duration**: 90 minutes +**Format**: Hands-on coding workshop +**Goal**: Build a complete E-Document solution with SimpleJson format and DirectionsConnector integration + +--- + +## Timeline + +| Time | Duration | Activity | +|------|----------|----------| +| 00:00-00:10 | 10 min | Introduction (using VS Code as presentation) | +| 00:10-00:40 | 30 min | Exercise 1 - SimpleJson Format Implementation | +| 00:40-01:10 | 30 min | Exercise 2 - DirectionsConnector Integration | +| 01:10-01:25 | 15 min | Testing & Live Demo | +| 01:25-01:30 | 5 min | Wrap-up & Q&A | + +--- + +## Architecture Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Business Central β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Sales Invoice │────────▢│ E-Document Core β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ SimpleJson Format β”‚ β”‚ +β”‚ β”‚ (Exercise 1) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ JSON Blob β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ DirectionsConnector β”‚ β”‚ +β”‚ β”‚ (Exercise 2) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ HTTP POST + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Azure API Server β”‚ + β”‚ (Pre-deployed) β”‚ + β”‚ β”‚ + β”‚ Endpoints: β”‚ + β”‚ - POST /register β”‚ + β”‚ - POST /enqueue β”‚ + β”‚ - GET /peek β”‚ + β”‚ - GET /dequeue β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## What Participants Will Build + +### Exercise 1: SimpleJson Format (30 minutes) +Implement the **"E-Document" Interface** to convert Business Central documents to/from JSON format. + +The interface has two main sections: +1. **Outgoing**: Convert BC documents to E-Document blobs (`Check`, `Create`, `CreateBatch`) +2. **Incoming**: Parse E-Document blobs to BC documents (`GetBasicInfoFromReceivedDocument`, `GetCompleteInfoFromReceivedDocument`) + +**Participants will implement:** +- βœ… `Check()` method - Validate required fields before document creation (5 min) +- βœ… `Create()` method - Generate JSON from Sales Invoice (15 min) +- βœ… `GetBasicInfoFromReceivedDocument()` method - Parse incoming JSON metadata (5 min) +- βœ… `GetCompleteInfoFromReceivedDocument()` method - Create Purchase Invoice from JSON (5 min) + +**Pre-written boilerplate includes:** +- Extension setup (app.json, dependencies) +- Enum extensions +- Helper methods for JSON generation and parsing +- Error handling framework + +### Exercise 2: DirectionsConnector Integration (30 minutes) +Implement the **Integration Interface** to send and receive documents via the Azure API server. + +**Participants will implement:** +- βœ… Connection setup and registration (5 min) +- βœ… `Send()` method - POST document to /enqueue endpoint (10 min) +- βœ… `ReceiveDocuments()` method - GET documents from /peek endpoint (10 min) +- βœ… `DownloadDocument()` method - GET single document from /dequeue endpoint (5 min) + +**Pre-written boilerplate includes:** +- Setup table and page UI +- Authentication helper +- HTTP request builders +- Error logging + +--- + +## Sample JSON Format + +Participants will generate JSON in this structure: + +```json +{ + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001", + "customerName": "Contoso Ltd.", + "postingDate": "2025-10-21", + "currencyCode": "USD", + "totalAmount": 1250.00, + "lines": [ + { + "lineNo": 1, + "type": "Item", + "no": "ITEM-001", + "description": "Item A", + "quantity": 5, + "unitPrice": 250.00, + "lineAmount": 1250.00 + } + ] +} +``` + +--- + +## Azure API Server + +**Base URL**: `https://[workshop-server].azurewebsites.net` (will be provided) + +### Endpoints + +#### 1. Register User +```http +POST /register +Content-Type: application/json + +{ + "name": "participant-name" +} + +Response: +{ + "status": "ok", + "key": "uuid-api-key" +} +``` + +#### 2. Send Document (Enqueue) +```http +POST /enqueue +X-Service-Key: your-api-key +Content-Type: application/json + +{ + "documentType": "Invoice", + "documentNo": "SI-001", + ... +} + +Response: +{ + "status": "ok", + "queued_count": 1 +} +``` + +#### 3. Check Queue +```http +GET /peek +X-Service-Key: your-api-key + +Response: +{ + "queued_count": 1, + "items": [...] +} +``` + +#### 4. Retrieve Document (Dequeue) +```http +GET /dequeue +X-Service-Key: your-api-key + +Response: +{ + "document": {...} +} +``` + +--- + +## Project Structure + +``` +DirectionsEMEA2025/ +β”œβ”€β”€ WORKSHOP_INTRO.md # VS Code presentation deck +β”œβ”€β”€ WORKSHOP_GUIDE.md # Step-by-step exercises +β”œβ”€β”€ API_REFERENCE.md # Azure API documentation +β”‚ +β”œβ”€β”€ application/ +β”‚ β”œβ”€β”€ simple_json/ # Exercise 1: Format Extension +β”‚ β”‚ β”œβ”€β”€ app.json +β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al +β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al # ⚠️ TODO sections +β”‚ β”‚ └── SimpleJsonHelper.Codeunit.al # βœ… Pre-written +β”‚ β”‚ +β”‚ └── directions_connector/ # Exercise 2: Integration Extension +β”‚ β”œβ”€β”€ app.json +β”‚ β”œβ”€β”€ DirectionsIntegration.EnumExt.al +β”‚ β”œβ”€β”€ DirectionsIntegration.Codeunit.al # ⚠️ TODO sections +β”‚ β”œβ”€β”€ DirectionsSetup.Table.al # βœ… Pre-written +β”‚ β”œβ”€β”€ DirectionsSetup.Page.al # βœ… Pre-written +β”‚ β”œβ”€β”€ DirectionsAuth.Codeunit.al # βœ… Pre-written +β”‚ └── DirectionsRequests.Codeunit.al # βœ… Pre-written +β”‚ +β”œβ”€β”€ solution/ # Complete working solution +β”‚ β”œβ”€β”€ simple_json/ # Reference implementation +β”‚ └── directions_connector/ # Reference implementation +β”‚ +└── server/ + β”œβ”€β”€ server.py # FastAPI server (for reference) + β”œβ”€β”€ requirements.txt + └── README.md # Deployment info (Azure hosted) +``` + +--- + +## Deliverables + +### 1. WORKSHOP_INTRO.md +VS Code presentation covering: +- **Slide 1**: Welcome & Goals +- **Slide 2**: E-Document Framework Overview +- **Slide 3**: Architecture Diagram +- **Slide 4**: SimpleJson Format Introduction +- **Slide 5**: DirectionsConnector Overview +- **Slide 6**: Azure API Server +- **Slide 7**: Exercise Overview +- **Slide 8**: Success Criteria + +### 2. WORKSHOP_GUIDE.md +Step-by-step instructions: +- Prerequisites & setup +- **Exercise 1**: SimpleJson Format (implements "E-Document" interface) + - Part A: Implement Check() (5 min) + - Part B: Implement Create() (15 min) + - Part C: Implement GetBasicInfoFromReceivedDocument() (5 min) + - Part D: Implement GetCompleteInfoFromReceivedDocument() (5 min) + - Validation steps +- **Exercise 2**: DirectionsConnector + - Part A: Register & Setup (5 min) + - Part B: Implement Send() (10 min) + - Part C: Implement ReceiveDocuments() (10 min) + - Part D: Implement DownloadDocument() (5 min) + - Validation steps +- Testing instructions +- Troubleshooting guide + +### 3. API_REFERENCE.md +Complete API documentation: +- Base URL and authentication +- All endpoint specifications +- Request/response examples +- Error handling +- Rate limits (if any) + +### 4. SimpleJson Format Boilerplate +Files with TODO sections: +- `app.json` - Extension manifest with dependencies +- `SimpleJsonFormat.EnumExt.al` - Enum extension (pre-written) +- `SimpleJsonFormat.Codeunit.al` - Format implementation with TODOs: + ```al + // TODO: Exercise 1.A - Implement validation + procedure Check(...) + begin + // TODO: Validate Customer No. + // TODO: Validate Posting Date + // TODO: Validate at least one line exists + end; + + // TODO: Exercise 1.B - Generate JSON + procedure Create(...) + begin + // TODO: Create JSON header + // TODO: Add lines array + // TODO: Calculate totals + end; + + // TODO: Exercise 1.C - Parse incoming JSON (Basic Info) + procedure GetBasicInfoFromReceivedDocument(...) + begin + // TODO: Parse JSON from TempBlob + // TODO: Set EDocument."Document Type" + // TODO: Set EDocument."Bill-to/Pay-to No." + // TODO: Set EDocument."Bill-to/Pay-to Name" + // TODO: Set EDocument."Document Date" + // TODO: Set EDocument."Currency Code" + end; + + // TODO: Exercise 1.D - Create Purchase Invoice (Complete Info) + procedure GetCompleteInfoFromReceivedDocument(...) + begin + // TODO: Read JSON from TempBlob + // TODO: Create Purchase Header record + // TODO: Set header fields from JSON + // TODO: Create Purchase Lines from JSON array + // TODO: Return via RecordRef parameters + end; + ``` +- `SimpleJsonHelper.Codeunit.al` - Helper methods (pre-written): + - `AddJsonProperty()` - Add property to JSON + - `StartJsonObject()` - Start JSON object + - `StartJsonArray()` - Start JSON array + - `ParseJsonValue()` - Get value from JSON + - `GetJsonToken()` - Get JSON token by path + - etc. + +### 5. DirectionsConnector Boilerplate +Files with TODO sections: +- `app.json` - Extension manifest +- `DirectionsIntegration.EnumExt.al` - Enum extension (pre-written) +- `DirectionsIntegration.Codeunit.al` - Integration implementation with TODOs: + ```al + // TODO: Exercise 2.A - Setup connection + local procedure RegisterUser(...) // Provided with TODOs + + // TODO: Exercise 2.B - Send document + procedure Send(...) + begin + // TODO: Get connection setup + // TODO: Prepare HTTP request + // TODO: Set authorization header + // TODO: Send POST request + // TODO: Handle response + end; + + // TODO: Exercise 2.C - Receive documents list + procedure ReceiveDocuments(...) + begin + // TODO: Get connection setup + // TODO: Call /peek endpoint + // TODO: Parse response and create metadata blobs + end; + + // TODO: Exercise 2.D - Download single document + procedure DownloadDocument(...) + begin + // TODO: Read document ID from metadata + // TODO: Call /dequeue endpoint + // TODO: Store document content in TempBlob + end; + ``` +- Pre-written helper files: + - `DirectionsSetup.Table.al` - Connection settings (URL, API Key) + - `DirectionsSetup.Page.al` - Setup UI + - `DirectionsAuth.Codeunit.al` - Authentication helper + - `DirectionsRequests.Codeunit.al` - HTTP request builders + +### 6. Solution Folder +Complete working implementations for instructor reference: +- `/solution/simple_json/` - Fully implemented format +- `/solution/directions_connector/` - Fully implemented connector +- These are NOT given to participants initially + +### 7. Server Documentation +- `server/README.md` - Overview of the API server + - Architecture explanation + - Endpoint documentation + - Deployment notes (Azure hosted) + - No setup required (server is pre-deployed) + +--- + +## TODO Marking Convention + +In code files, use clear TODO markers with timing: + +```al +// ============================================================================ +// TODO: Exercise 1.A (10 minutes) +// Validate that required fields are filled before creating the document +// +// Instructions: +// 1. Validate that Customer No. is not empty +// 2. Validate that Posting Date is set +// 3. Validate that at least one line exists +// +// Hints: +// - Use SourceDocumentHeader.Field(FieldNo).TestField() +// - Use EDocumentErrorHelper.LogSimpleErrorMessage() for custom errors +// - Check the README.md for the Check() method example +// ============================================================================ +procedure Check(var SourceDocumentHeader: RecordRef; EDocumentService: Record "E-Document Service"; EDocumentProcessingPhase: Enum "E-Document Processing Phase") +begin + // TODO: Your code here +end; +``` + +--- + +## Success Criteria + +By the end of the workshop, participants should be able to: +- βœ… Create a sales invoice in BC +- βœ… See it converted to JSON via SimpleJson format +- βœ… Send it to Azure API server via DirectionsConnector +- βœ… Verify it appears in the queue (via /peek endpoint) +- βœ… Receive documents from the Azure API server +- βœ… Parse incoming JSON and extract metadata +- βœ… Create purchase invoices from received E-Documents +- βœ… Understand the complete E-Document round-trip flow (outgoing and incoming) +- βœ… Understand the E-Document framework architecture +- βœ… Know how to extend with additional features + +--- + +## Bonus/Homework Ideas + +For advanced participants or post-workshop: + +**Format Interface:** +- Implement `CreateBatch()` for batch processing multiple documents +- Add support for Sales Credit Memos and Purchase Credit Memos +- Add more sophisticated field mappings (dimensions, custom fields) +- Implement validation rules for incoming documents +- Support for attachments (PDF, XML) + +**Integration Interface:** +- Implement `IDocumentResponseHandler` with `GetResponse()` for async status checking +- Implement `ISentDocumentActions` for approval/cancellation workflows +- Implement `IDocumentAction` for custom actions +- Add comprehensive error handling and retry logic +- Add batch sending support + +--- + +## Notes for Implementation + +### Key Simplifications for 90-Minute Workshop +1. **Format**: Full round-trip (Create and PrepareDocument), but simplified field mapping +2. **Connector**: Full round-trip (Send and Receive), but simplified response handling +3. **Validation**: Basic field checks only +4. **Error Handling**: Use pre-written helpers +5. **UI**: Minimal - focus on code logic +6. **Document Types**: Only Sales Invoice outgoing, only Purchase Invoice incoming + +### Pre-Written vs TODO +**Participants write** (~60% of time): +- Business validation logic +- JSON structure generation +- HTTP request preparation +- Response handling + +**Pre-written boilerplate** (~40% setup time saved): +- Extension setup and dependencies +- Enum extensions +- Setup tables/pages +- Helper methods (JSON, HTTP, Auth) +- Error logging framework + +### Testing Strategy + +**Outgoing Flow (Send):** +1. Create test sales invoice with known data +2. Post and verify E-Document created +3. Check JSON blob content in E-Document log +4. Verify document sent to Azure API +5. Use /peek endpoint to confirm receipt in queue + +**Incoming Flow (Receive):** +6. Use another participant's queue or test data in API +7. Run "Get Documents" action in BC +8. Verify E-Documents appear in E-Document list with status "Imported" +9. Check downloaded JSON content in E-Document log +10. Verify basic info parsed correctly (vendor, amount, date) +11. Create Purchase Invoice from E-Document +12. Verify purchase invoice created with correct data +13. Confirm document removed from queue (via /peek) + +--- + +## Files to Create + +### Immediate (for workshop) +- [ ] `WORKSHOP_INTRO.md` - VS Code presentation +- [ ] `WORKSHOP_GUIDE.md` - Exercise instructions +- [ ] `API_REFERENCE.md` - Azure API docs +- [ ] `application/simple_json/` - Format boilerplate +- [ ] `application/directions_connector/` - Connector boilerplate + +### Reference (for instructor) +- [ ] `solution/simple_json/` - Complete format +- [ ] `solution/directions_connector/` - Complete connector +- [ ] `server/README.md` - Server documentation + +### Nice-to-have +- [ ] `TROUBLESHOOTING.md` - Common issues +- [ ] `HOMEWORK.md` - Post-workshop exercises +- [ ] Sample test data scripts + +--- + +## Next Steps + +1. Review and approve this plan +2. Get Azure server URL and deployment details +3. Create the workshop introduction (WORKSHOP_INTRO.md) +4. Create the boilerplate code with TODOs +5. Create the complete solution for reference +6. Test the full workshop flow +7. Prepare any PowerPoint backup slides (if needed) + +--- + +**Ready to implement?** Let me know if you want to adjust anything before we start building! diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsAuth.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsAuth.Codeunit.al new file mode 100644 index 00000000..caff8a8c --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsAuth.Codeunit.al @@ -0,0 +1,122 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Integration; + +using System.Utilities; + +/// +/// Helper codeunit for authentication and registration with the Directions API. +/// Pre-written to save time during the workshop. +/// +codeunit 81101 "Directions Auth" +{ + Access = Internal; + + /// + /// Registers a new user with the Directions API and stores the API key. + /// + procedure RegisterUser(var DirectionsSetup: Record "Directions Connection Setup") + var + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + HttpContent: HttpContent; + JsonObject: JsonObject; + JsonToken: JsonToken; + ResponseText: Text; + RequestBody: Text; + begin + if DirectionsSetup."API Base URL" = '' then + Error('Please specify the API Base URL before registering.'); + + if DirectionsSetup."User Name" = '' then + Error('Please specify a User Name before registering.'); + + // Create request body + JsonObject.Add('name', DirectionsSetup."User Name"); + JsonObject.WriteTo(RequestBody); + + // Prepare HTTP request + HttpContent.WriteFrom(RequestBody); + HttpContent.GetHeaders().Remove('Content-Type'); + HttpContent.GetHeaders().Add('Content-Type', 'application/json'); + + HttpRequest.Method := 'POST'; + HttpRequest.SetRequestUri(DirectionsSetup."API Base URL" + 'register'); + HttpRequest.Content := HttpContent; + + // Send request + if not HttpClient.Send(HttpRequest, HttpResponse) then + Error('Failed to connect to the API server.'); + + // Parse response + HttpResponse.Content.ReadAs(ResponseText); + + if not HttpResponse.IsSuccessStatusCode() then + Error('Registration failed: %1', ResponseText); + + // Extract API key from response + if JsonObject.ReadFrom(ResponseText) then begin + if JsonObject.Get('key', JsonToken) then begin + DirectionsSetup.SetAPIKey(JsonToken.AsValue().AsText()); + DirectionsSetup.Registered := true; + DirectionsSetup.Modify(); + end else + Error('API key not found in response.'); + end else + Error('Invalid response format from API.'); + end; + + /// + /// Tests the connection to the API by calling the /peek endpoint. + /// + procedure TestConnection(DirectionsSetup: Record "Directions Connection Setup") + var + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + begin + if DirectionsSetup."API Base URL" = '' then + Error('Please specify the API Base URL.'); + + if not DirectionsSetup.Registered then + Error('Please register first to get an API key.'); + + // Prepare HTTP request + HttpRequest.Method := 'GET'; + HttpRequest.SetRequestUri(DirectionsSetup."API Base URL" + 'peek'); + AddAuthHeader(HttpRequest, DirectionsSetup); + + // Send request + if not HttpClient.Send(HttpRequest, HttpResponse) then + Error('Failed to connect to the API server.'); + + if not HttpResponse.IsSuccessStatusCode() then + Error('Connection test failed with status: %1', HttpResponse.HttpStatusCode()); + end; + + /// + /// Adds the authentication header to an HTTP request. + /// + procedure AddAuthHeader(var HttpRequest: HttpRequestMessage; DirectionsSetup: Record "Directions Connection Setup") + begin + HttpRequest.GetHeaders().Add('X-Service-Key', DirectionsSetup.GetAPIKeyText()); + end; + + /// + /// Gets the connection setup record, ensuring it exists. + /// + procedure GetConnectionSetup(var DirectionsSetup: Record "Directions Connection Setup") + begin + if not DirectionsSetup.Get() then + Error('Directions Connector is not configured. Please open the Directions Connection Setup page.'); + + if DirectionsSetup."API Base URL" = '' then + Error('API Base URL is not configured.'); + + if not DirectionsSetup.Registered then + Error('Not registered with the API. Please register first.'); + end; +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Page.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Page.al new file mode 100644 index 00000000..bb9e9d29 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Page.al @@ -0,0 +1,138 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Integration; + +/// +/// Setup page for DirectionsConnector API connection. +/// Allows users to configure the API URL and register to get an API key. +/// +page 81100 "Directions Connection Setup" +{ + Caption = 'Directions Connector Setup'; + PageType = Card; + SourceTable = "Directions Connection Setup"; + InsertAllowed = false; + DeleteAllowed = false; + ApplicationArea = All; + UsageCategory = Administration; + + layout + { + area(content) + { + group(General) + { + Caption = 'Connection Settings'; + + field("API Base URL"; Rec."API Base URL") + { + ApplicationArea = All; + ToolTip = 'Specifies the base URL for the Directions API server (e.g., https://workshop-server.azurewebsites.net/)'; + ShowMandatory = true; + } + field("User Name"; Rec."User Name") + { + ApplicationArea = All; + ToolTip = 'Specifies the user name to register with the API'; + ShowMandatory = true; + } + field(Registered; Rec.Registered) + { + ApplicationArea = All; + ToolTip = 'Indicates whether you have successfully registered with the API'; + Style = Favorable; + StyleExpr = Rec.Registered; + } + field("API Key"; APIKeyText) + { + ApplicationArea = All; + Caption = 'API Key'; + ToolTip = 'Specifies the API key received after registration'; + Editable = false; + ExtendedDatatype = Masked; + + trigger OnAssistEdit() + var + NewAPIKey: Text; + begin + NewAPIKey := APIKeyText; + if NewAPIKey <> '' then begin + Rec.SetAPIKey(NewAPIKey); + Rec.Modify(); + CurrPage.Update(); + end; + end; + } + } + } + } + + actions + { + area(Processing) + { + action(Register) + { + ApplicationArea = All; + Caption = 'Register'; + ToolTip = 'Register with the Directions API to get an API key'; + Image = Approve; + Promoted = true; + PromotedCategory = Process; + PromotedOnly = true; + + trigger OnAction() + var + DirectionsAuth: Codeunit "Directions Auth"; + begin + DirectionsAuth.RegisterUser(Rec); + CurrPage.Update(); + UpdateAPIKeyText(); + Message('Registration successful! Your API key has been saved.'); + end; + } + action(TestConnection) + { + ApplicationArea = All; + Caption = 'Test Connection'; + ToolTip = 'Test the connection to the Directions API'; + Image = ValidateEmailLoggingSetup; + Promoted = true; + PromotedCategory = Process; + PromotedOnly = true; + + trigger OnAction() + var + DirectionsAuth: Codeunit "Directions Auth"; + begin + DirectionsAuth.TestConnection(Rec); + Message('Connection test successful!'); + end; + } + } + } + + trigger OnOpenPage() + begin + Rec.GetOrCreate(); + UpdateAPIKeyText(); + end; + + trigger OnAfterGetCurrRecord() + begin + UpdateAPIKeyText(); + end; + + local procedure UpdateAPIKeyText() + begin + if not IsNullGuid(Rec."API Key") then + APIKeyText := Rec.GetAPIKeyText() + else + APIKeyText := ''; + end; + + var + APIKeyText: Text; +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Table.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Table.al new file mode 100644 index 00000000..a4df6d18 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Table.al @@ -0,0 +1,94 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Integration; + +/// +/// Stores connection settings for DirectionsConnector API. +/// This is a singleton table that holds the API URL and authentication key. +/// +table 81100 "Directions Connection Setup" +{ + Caption = 'Directions Connection Setup'; + DataClassification = CustomerContent; + + fields + { + field(1; "Primary Key"; Code[10]) + { + Caption = 'Primary Key'; + DataClassification = SystemMetadata; + } + field(10; "API Base URL"; Text[250]) + { + Caption = 'API Base URL'; + DataClassification = CustomerContent; + + trigger OnValidate() + begin + if "API Base URL" <> '' then + if not "API Base URL".EndsWith('/') then + "API Base URL" := "API Base URL" + '/'; + end; + } + field(11; "API Key"; Guid) + { + Caption = 'API Key'; + DataClassification = EndUserIdentifiableInformation; + } + field(20; "User Name"; Text[100]) + { + Caption = 'User Name'; + DataClassification = EndUserIdentifiableInformation; + } + field(21; Registered; Boolean) + { + Caption = 'Registered'; + DataClassification = CustomerContent; + Editable = false; + } + } + + keys + { + key(PK; "Primary Key") + { + Clustered = true; + } + } + + /// + /// Gets or creates the singleton setup record. + /// + procedure GetOrCreate(): Boolean + begin + if not Get() then begin + Init(); + "Primary Key" := ''; + Insert(); + end; + exit(true); + end; + + /// + /// Sets the API key from text value. + /// + procedure SetAPIKey(NewAPIKey: Text) + var + APIKeyGuid: Guid; + begin + if Evaluate(APIKeyGuid, NewAPIKey) then + "API Key" := APIKeyGuid + else + Error('Invalid API Key format. Must be a valid GUID.'); + end; + + /// + /// Gets the API key as text. + /// + procedure GetAPIKeyText(): Text + begin + exit(Format("API Key")); + end; +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.Codeunit.al new file mode 100644 index 00000000..23207355 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.Codeunit.al @@ -0,0 +1,157 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Integration; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Integration.Send; +using Microsoft.eServices.EDocument.Integration.Receive; +using Microsoft.eServices.EDocument.Integration.Interfaces; +using System.Utilities; + +/// +/// Implements the IDocumentSender and IDocumentReceiver interfaces for DirectionsConnector. +/// This codeunit handles sending and receiving E-Documents via the Directions API. +/// +codeunit 81100 "Directions Integration Impl." implements IDocumentSender, IDocumentReceiver +{ + Access = Internal; + + // ============================================================================ + // SENDING DOCUMENTS + // ============================================================================ + + // ============================================================================ + // TODO: Exercise 2.B (10 minutes) + // Send an E-Document to the Directions API. + // + // TASK: Uncomment the code below - it's already complete! + // ============================================================================ + procedure Send(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; SendContext: Codeunit SendContext) + var + DirectionsSetup: Record "Directions Connection Setup"; + DirectionsAuth: Codeunit "Directions Auth"; + DirectionsRequests: Codeunit "Directions Requests"; + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + TempBlob: Codeunit "Temp Blob"; + JsonContent: Text; + begin + // TODO: Uncomment all the code below + // DirectionsAuth.GetConnectionSetup(DirectionsSetup); + // SendContext.GetTempBlob(TempBlob); + // JsonContent := DirectionsRequests.ReadJsonFromBlob(TempBlob); + // DirectionsRequests.CreatePostRequest(DirectionsSetup."API Base URL" + 'enqueue', JsonContent, HttpRequest); + // DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); + // SendContext.Http().SetHttpRequestMessage(HttpRequest); + // if not HttpClient.Send(HttpRequest, HttpResponse) then + // Error('Failed to send document to API.'); + // SendContext.Http().SetHttpResponseMessage(HttpResponse); + // DirectionsRequests.CheckResponseSuccess(HttpResponse); + end; + + // ============================================================================ + // RECEIVING DOCUMENTS + // ============================================================================ + + // ============================================================================ + // TODO: Exercise 2.C (10 minutes) + // Receive a list of documents from the Directions API. + // + // TASK: Uncomment the code below and fill in the ??? with the correct endpoint + // ============================================================================ + procedure ReceiveDocuments(var EDocumentService: Record "E-Document Service"; DocumentsMetadata: Codeunit "Temp Blob List"; ReceiveContext: Codeunit ReceiveContext) + var + DirectionsSetup: Record "Directions Connection Setup"; + DirectionsAuth: Codeunit "Directions Auth"; + DirectionsRequests: Codeunit "Directions Requests"; + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + JsonObject: JsonObject; + JsonToken: JsonToken; + JsonArray: JsonArray; + TempBlob: Codeunit "Temp Blob"; + ResponseText: Text; + DocumentJson: Text; + begin + // TODO: Uncomment the code below and replace ??? with 'peek' + // DirectionsAuth.GetConnectionSetup(DirectionsSetup); + // DirectionsRequests.CreateGetRequest(DirectionsSetup."API Base URL" + '???', HttpRequest); // TODO: What endpoint shows the queue? + // DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); + // ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); + // if not HttpClient.Send(HttpRequest, HttpResponse) then + // Error('Failed to retrieve documents from API.'); + // ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); + // DirectionsRequests.CheckResponseSuccess(HttpResponse); + // ResponseText := DirectionsRequests.GetResponseText(HttpResponse); + // if JsonObject.ReadFrom(ResponseText) then begin + // if JsonObject.Get('items', JsonToken) then begin + // JsonArray := JsonToken.AsArray(); + // foreach JsonToken in JsonArray do begin + // Clear(TempBlob); + // JsonToken.WriteTo(DocumentJson); + // DirectionsRequests.WriteTextToBlob(DocumentJson, TempBlob); + // DocumentsMetadata.Add(TempBlob); + // end; + // end; + // end; + end; + + // ============================================================================ + // TODO: Exercise 2.D (5 minutes) + // Download a single document from the Directions API (dequeue). + // + // TASK: Uncomment the code below and fill in ??? with the correct endpoint + // ============================================================================ + procedure DownloadDocument(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; DocumentMetadata: codeunit "Temp Blob"; ReceiveContext: Codeunit ReceiveContext) + var + DirectionsSetup: Record "Directions Connection Setup"; + DirectionsAuth: Codeunit "Directions Auth"; + DirectionsRequests: Codeunit "Directions Requests"; + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + JsonObject: JsonObject; + JsonToken: JsonToken; + TempBlob: Codeunit "Temp Blob"; + ResponseText: Text; + DocumentJson: Text; + begin + // TODO: Uncomment the code below and replace ??? with 'dequeue' + // DirectionsAuth.GetConnectionSetup(DirectionsSetup); + // DirectionsRequests.CreateGetRequest(DirectionsSetup."API Base URL" + '???', HttpRequest); // TODO: What endpoint removes from queue? + // DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); + // ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); + // if not HttpClient.Send(HttpRequest, HttpResponse) then + // Error('Failed to download document from API.'); + // ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); + // DirectionsRequests.CheckResponseSuccess(HttpResponse); + // ResponseText := DirectionsRequests.GetResponseText(HttpResponse); + // if JsonObject.ReadFrom(ResponseText) then begin + // if JsonObject.Get('document', JsonToken) then begin + // JsonToken.WriteTo(DocumentJson); + // ReceiveContext.GetTempBlob(TempBlob); + // DirectionsRequests.WriteTextToBlob(DocumentJson, TempBlob); + // end else + // Error('No document found in response.'); + // end; + end; + + // ============================================================================ + // Event Subscriber - Opens setup page when configuring the service + // ============================================================================ + [EventSubscriber(ObjectType::Page, Page::"E-Document Service", OnBeforeOpenServiceIntegrationSetupPage, '', false, false)] + local procedure OnBeforeOpenServiceIntegrationSetupPage(EDocumentService: Record "E-Document Service"; var IsServiceIntegrationSetupRun: Boolean) + var + DirectionsSetup: Page "Directions Connection Setup"; + begin + if EDocumentService."Service Integration V2" <> EDocumentService."Service Integration V2"::"Directions Connector" then + exit; + + DirectionsSetup.RunModal(); + IsServiceIntegrationSetupRun := true; + end; +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.EnumExt.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.EnumExt.al new file mode 100644 index 00000000..548972b1 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.EnumExt.al @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Integration; + +using Microsoft.eServices.EDocument.Integration.Interfaces; + +/// +/// Extends the Service Integration V2 enum to add DirectionsConnector integration. +/// This enum value is used to identify which integration implementation to use. +/// +enumextension 81100 "Directions Integration" extends "Service Integration V2" +{ + value(81100; "Directions Connector") + { + Caption = 'Directions Connector'; + Implementation = IDocumentSender = "Directions Integration Impl.", + IDocumentReceiver = "Directions Integration Impl."; + } +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsRequests.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsRequests.Codeunit.al new file mode 100644 index 00000000..8b2e665d --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsRequests.Codeunit.al @@ -0,0 +1,89 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Integration; + +using System.Utilities; + +/// +/// Helper codeunit for building HTTP requests for the Directions API. +/// Pre-written to save time during the workshop. +/// +codeunit 81102 "Directions Requests" +{ + Access = Internal; + + /// + /// Creates an HTTP POST request with JSON content. + /// + procedure CreatePostRequest(Url: Text; JsonContent: Text; var HttpRequest: HttpRequestMessage) + var + HttpContent: HttpContent; + begin + HttpContent.WriteFrom(JsonContent); + HttpContent.GetHeaders().Remove('Content-Type'); + HttpContent.GetHeaders().Add('Content-Type', 'application/json'); + + HttpRequest.Method := 'POST'; + HttpRequest.SetRequestUri(Url); + HttpRequest.Content := HttpContent; + end; + + /// + /// Creates an HTTP GET request. + /// + procedure CreateGetRequest(Url: Text; var HttpRequest: HttpRequestMessage) + begin + HttpRequest.Method := 'GET'; + HttpRequest.SetRequestUri(Url); + end; + + /// + /// Reads the response content as text. + /// + procedure GetResponseText(HttpResponse: HttpResponseMessage): Text + var + ResponseText: Text; + begin + HttpResponse.Content.ReadAs(ResponseText); + exit(ResponseText); + end; + + /// + /// Checks if the response is successful and throws an error if not. + /// + procedure CheckResponseSuccess(HttpResponse: HttpResponseMessage) + var + ResponseText: Text; + begin + if not HttpResponse.IsSuccessStatusCode() then begin + ResponseText := GetResponseText(HttpResponse); + Error('API request failed with status %1: %2', HttpResponse.HttpStatusCode(), ResponseText); + end; + end; + + /// + /// Reads JSON content from a TempBlob. + /// + procedure ReadJsonFromBlob(var TempBlob: Codeunit "Temp Blob"): Text + var + InStr: InStream; + JsonText: Text; + begin + TempBlob.CreateInStream(InStr, TextEncoding::UTF8); + InStr.ReadText(JsonText); + exit(JsonText); + end; + + /// + /// Writes text content to a TempBlob. + /// + procedure WriteTextToBlob(TextContent: Text; var TempBlob: Codeunit "Temp Blob") + var + OutStr: OutStream; + begin + TempBlob.CreateOutStream(OutStr, TextEncoding::UTF8); + OutStr.WriteText(TextContent); + end; +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/app.json b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/app.json new file mode 100644 index 00000000..16003bae --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/app.json @@ -0,0 +1,32 @@ +{ + "id": "12345678-0002-0002-0002-000000000002", + "name": "Directions Connector", + "publisher": "Directions EMEA Workshop", + "version": "1.0.0.0", + "brief": "DirectionsConnector integration for E-Document workshop", + "description": "Workshop extension that implements the E-Document integration interfaces to send/receive documents via HTTP API", + "privacyStatement": "https://privacy.microsoft.com", + "EULA": "https://www.microsoft.com/en-us/legal/intellectualproperty/copyright", + "help": "https://learn.microsoft.com/dynamics365/business-central/", + "url": "https://github.com/microsoft/BCTech", + "logo": "", + "dependencies": [ + { + "id": "e1d97edc-c239-46b4-8d84-6368bdf67c8b", + "name": "E-Document Core", + "publisher": "Microsoft", + "version": "25.0.0.0" + } + ], + "screenshots": [], + "platform": "25.0.0.0", + "idRanges": [ + { + "from": 81100, + "to": 81199 + } + ], + "features": [], + "target": "OnPrem", + "runtime": "13.0" +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al new file mode 100644 index 00000000..04317634 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al @@ -0,0 +1,240 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Format; + +using Microsoft.eServices.EDocument; +using Microsoft.Sales.History; +using Microsoft.Purchases.Document; +using System.Utilities; + +/// +/// Implements the "E-Document" interface for SimpleJson format. +/// This codeunit converts Business Central documents to/from JSON format. +/// +codeunit 81000 "SimpleJson Format Impl." implements "E-Document" +{ + Access = Internal; + + // ============================================================================ + // OUTGOING DOCUMENTS - Convert BC documents to JSON + // ============================================================================ + + // ============================================================================ + // TODO: Exercise 1.A (5 minutes) + // Validate that required fields are filled before creating the document. + // + // TASK: Uncomment the two validation lines below + // ============================================================================ + procedure Check(var SourceDocumentHeader: RecordRef; EDocumentService: Record "E-Document Service"; EDocumentProcessingPhase: Enum "E-Document Processing Phase") + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + begin + case SourceDocumentHeader.Number of + Database::"Sales Invoice Header": + begin + // TODO: Uncomment these two lines to validate required fields: + // SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Sell-to Customer No.")).TestField(); + // SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Posting Date")).TestField(); + end; + end; + end; + + // ============================================================================ + // TODO: Exercise 1.B (15 minutes) + // Create JSON representation of a Sales Invoice. + // + // TASK: Fill in the missing values marked with ??? in the code below + // ============================================================================ + procedure Create(EDocumentService: Record "E-Document Service"; var EDocument: Record "E-Document"; var SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; var TempBlob: Codeunit "Temp Blob") + var + OutStr: OutStream; + begin + TempBlob.CreateOutStream(OutStr, TextEncoding::UTF8); + + case EDocument."Document Type" of + EDocument."Document Type"::"Sales Invoice": + CreateSalesInvoiceJson(SourceDocumentHeader, SourceDocumentLines, OutStr); + else + Error('Document type %1 is not supported', EDocument."Document Type"); + end; + end; + + local procedure CreateSalesInvoiceJson(var SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; var OutStr: OutStream) + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + RootObject: JsonObject; + LinesArray: JsonArray; + LineObject: JsonObject; + JsonText: Text; + begin + // Get the actual records from RecordRef + SourceDocumentHeader.SetTable(SalesInvoiceHeader); + SourceDocumentLines.SetTable(SalesInvoiceLine); + + // TODO: Add header fields - Replace ??? with the correct field names + RootObject.Add('documentType', 'Invoice'); + RootObject.Add('documentNo', SalesInvoiceHeader."No."); + RootObject.Add('customerNo', SalesInvoiceHeader."Sell-to Customer No."); + RootObject.Add('customerName', SalesInvoiceHeader."???"); // TODO: What field contains customer name? + RootObject.Add('postingDate', Format(SalesInvoiceHeader."Posting Date", 0, '--')); + RootObject.Add('currencyCode', SalesInvoiceHeader."Currency Code"); + RootObject.Add('totalAmount', SalesInvoiceHeader."???"); // TODO: What field has the total amount? + + // Create lines array + if SalesInvoiceLine.FindSet() then + repeat + // TODO: Add line item - Replace ??? with correct field names + Clear(LineObject); + LineObject.Add('lineNo', SalesInvoiceLine."Line No."); + LineObject.Add('type', Format(SalesInvoiceLine.Type)); + LineObject.Add('no', SalesInvoiceLine."No."); + LineObject.Add('description', SalesInvoiceLine."???"); // TODO: What field has the description? + LineObject.Add('quantity', SalesInvoiceLine."???"); // TODO: What field has quantity? + LineObject.Add('unitPrice', SalesInvoiceLine."Unit Price"); + LineObject.Add('lineAmount', SalesInvoiceLine."Amount Including VAT"); + LinesArray.Add(LineObject); + until SalesInvoiceLine.Next() = 0; + + // Add lines array to root object + RootObject.Add('lines', LinesArray); + + // Write JSON to stream + RootObject.WriteTo(JsonText); + OutStr.WriteText(JsonText); + end; + + // This method is for batch processing (optional/advanced) + procedure CreateBatch(EDocumentService: Record "E-Document Service"; var EDocuments: Record "E-Document"; var SourceDocumentHeaders: RecordRef; var SourceDocumentsLines: RecordRef; var TempBlob: Codeunit "Temp Blob") + begin + // Not implemented for workshop - can be homework + Error('Batch creation is not implemented in this workshop version'); + end; + + // ============================================================================ + // INCOMING DOCUMENTS - Parse JSON to BC documents + // ============================================================================ + + // ============================================================================ + // TODO: Exercise 1.C (5 minutes) + // Parse basic information from received JSON document. + // + // TASK: Fill in the missing JSON field names marked with ??? + // ============================================================================ + procedure GetBasicInfoFromReceivedDocument(var EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob") + var + JsonObject: JsonObject; + JsonToken: JsonToken; + SimpleJsonHelper: Codeunit "SimpleJson Helper"; + begin + if not SimpleJsonHelper.ReadJsonFromBlob(TempBlob, JsonObject) then + Error('Failed to parse JSON document'); + + // Set document type to Purchase Invoice + EDocument."Document Type" := EDocument."Document Type"::"Purchase Invoice"; + + // Extract document number + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'documentNo', JsonToken) then + EDocument."Document No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Document No.")); + + // TODO: Extract vendor number (from "customerNo" in JSON) - Replace ??? with the JSON field name + if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then + EDocument."Bill-to/Pay-to No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Bill-to/Pay-to No.")); + + // TODO: Extract vendor name - Replace ??? with the JSON field name + if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then + EDocument."Bill-to/Pay-to Name" := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Bill-to/Pay-to Name")); + + // Extract posting date + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then + EDocument."Document Date" := SimpleJsonHelper.GetJsonTokenDate(JsonToken); + + // Extract currency code + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'currencyCode', JsonToken) then + EDocument."Currency Code" := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Currency Code")); + end; + + // ============================================================================ + // TODO: Exercise 1.D (5 minutes) + // Create a Purchase Invoice from received JSON document. + // + // TASK: Fill in the missing field names marked with ??? + // ============================================================================ + procedure GetCompleteInfoFromReceivedDocument(var EDocument: Record "E-Document"; var CreatedDocumentHeader: RecordRef; var CreatedDocumentLines: RecordRef; var TempBlob: Codeunit "Temp Blob") + var + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + JsonObject: JsonObject; + JsonToken: JsonToken; + JsonArray: JsonArray; + JsonLineToken: JsonToken; + SimpleJsonHelper: Codeunit "SimpleJson Helper"; + LineNo: Integer; + begin + TODO: Uncomment all the code below + if not SimpleJsonHelper.ReadJsonFromBlob(TempBlob, JsonObject) then + Error('Failed to parse JSON document'); + + // Create Purchase Header + PurchaseHeader.Init(); + PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::Invoice; + PurchaseHeader.Insert(true); + + // Set vendor from JSON + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerNo', JsonToken) then + PurchaseHeader.Validate("Buy-from Vendor No.", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); + + // Set posting date + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then + PurchaseHeader.Validate("Posting Date", SimpleJsonHelper.GetJsonTokenDate(JsonToken)); + + // Set currency code (if not blank) + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'currencyCode', JsonToken) then begin + if SimpleJsonHelper.GetJsonTokenValue(JsonToken) <> '' then + PurchaseHeader.Validate("Currency Code", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); + end; + + PurchaseHeader.Modify(true); + + // Create Purchase Lines from JSON array + if JsonObject.Get('lines', JsonToken) then begin + JsonArray := JsonToken.AsArray(); + LineNo := 10000; + + foreach JsonLineToken in JsonArray do begin + JsonObject := JsonLineToken.AsObject(); + + PurchaseLine.Init(); + PurchaseLine."Document Type" := PurchaseHeader."Document Type"; + PurchaseLine."Document No." := PurchaseHeader."No."; + PurchaseLine."Line No." := LineNo; + PurchaseLine.Type := PurchaseLine.Type::Item; + + // TODO: Set item number - Replace ??? with the JSON field name for item number + if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then + PurchaseLine.Validate("No.", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); + + // Set description + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'description', JsonToken) then + PurchaseLine.Description := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(PurchaseLine.Description)); + + // TODO: Set quantity - Replace ??? with the JSON field name for quantity + if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then + PurchaseLine.Validate(Quantity, SimpleJsonHelper.GetJsonTokenDecimal(JsonToken)); + + // Set unit cost + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'unitPrice', JsonToken) then + PurchaseLine.Validate("Direct Unit Cost", SimpleJsonHelper.GetJsonTokenDecimal(JsonToken)); + + PurchaseLine.Insert(true); + LineNo += 10000; + end; + end; + + // Return via RecordRef + CreatedDocumentHeader.GetTable(PurchaseHeader); + CreatedDocumentLines.GetTable(PurchaseLine); + end; +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.EnumExt.al b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.EnumExt.al new file mode 100644 index 00000000..77a561a8 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.EnumExt.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Format; + +using Microsoft.eServices.EDocument; + +/// +/// Extends the E-Document Format enum to add SimpleJson format. +/// This enum value is used to identify which format implementation to use. +/// +enumextension 81000 "SimpleJson Format" extends "E-Document Format" +{ + value(81000; "SimpleJson") + { + Caption = 'Simple JSON Format'; + Implementation = "E-Document" = "SimpleJson Format Impl."; + } +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonHelper.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonHelper.Codeunit.al new file mode 100644 index 00000000..5377419c --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonHelper.Codeunit.al @@ -0,0 +1,102 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Format; + +using System.Utilities; + +/// +/// Helper codeunit with pre-written methods for JSON operations. +/// These methods are provided to save time during the workshop. +/// +codeunit 81001 "SimpleJson Helper" +{ + Access = Internal; + + /// + /// Writes a JSON property to the output stream. + /// + /// The output stream to write to. + /// The name of the JSON property. + /// The value of the JSON property. + procedure WriteJsonProperty(var OutStr: OutStream; PropertyName: Text; PropertyValue: Text) + begin + OutStr.WriteText('"' + PropertyName + '": "' + PropertyValue + '"'); + end; + + /// + /// Writes a JSON numeric property to the output stream. + /// + procedure WriteJsonNumericProperty(var OutStr: OutStream; PropertyName: Text; PropertyValue: Decimal) + begin + OutStr.WriteText('"' + PropertyName + '": ' + Format(PropertyValue, 0, 9)); + end; + + /// + /// Gets a text value from a JSON token. + /// + procedure GetJsonTokenValue(JsonToken: JsonToken): Text + var + JsonValue: JsonValue; + begin + if JsonToken.IsValue then begin + JsonValue := JsonToken.AsValue(); + exit(JsonValue.AsText()); + end; + exit(''); + end; + + /// + /// Gets a decimal value from a JSON token. + /// + procedure GetJsonTokenDecimal(JsonToken: JsonToken): Decimal + var + JsonValue: JsonValue; + DecimalValue: Decimal; + begin + if JsonToken.IsValue then begin + JsonValue := JsonToken.AsValue(); + if JsonValue.AsDecimal(DecimalValue) then + exit(DecimalValue); + end; + exit(0); + end; + + /// + /// Gets a date value from a JSON token. + /// + procedure GetJsonTokenDate(JsonToken: JsonToken): Date + var + JsonValue: JsonValue; + DateValue: Date; + begin + if JsonToken.IsValue then begin + JsonValue := JsonToken.AsValue(); + if Evaluate(DateValue, JsonValue.AsText()) then + exit(DateValue); + end; + exit(0D); + end; + + /// + /// Selects a JSON token from a JSON object by path. + /// + procedure SelectJsonToken(JsonObject: JsonObject; Path: Text; var JsonToken: JsonToken): Boolean + begin + exit(JsonObject.SelectToken(Path, JsonToken)); + end; + + /// + /// Reads JSON content from a TempBlob into a JsonObject. + /// + procedure ReadJsonFromBlob(var TempBlob: Codeunit "Temp Blob"; var JsonObject: JsonObject): Boolean + var + InStr: InStream; + JsonText: Text; + begin + TempBlob.CreateInStream(InStr, TextEncoding::UTF8); + InStr.ReadText(JsonText); + exit(JsonObject.ReadFrom(JsonText)); + end; +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/app.json b/samples/EDocument/DirectionsEMEA2025/application/simple_json/app.json new file mode 100644 index 00000000..bc84f236 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/app.json @@ -0,0 +1,32 @@ +{ + "id": "12345678-0001-0001-0001-000000000001", + "name": "SimpleJson E-Document Format", + "publisher": "Directions EMEA Workshop", + "version": "1.0.0.0", + "brief": "SimpleJson format for E-Document workshop", + "description": "Workshop extension that implements the E-Document format interface for JSON documents", + "privacyStatement": "https://privacy.microsoft.com", + "EULA": "https://www.microsoft.com/en-us/legal/intellectualproperty/copyright", + "help": "https://learn.microsoft.com/dynamics365/business-central/", + "url": "https://github.com/microsoft/BCTech", + "logo": "", + "dependencies": [ + { + "id": "e1d97edc-c239-46b4-8d84-6368bdf67c8b", + "name": "E-Document Core", + "publisher": "Microsoft", + "version": "25.0.0.0" + } + ], + "screenshots": [], + "platform": "25.0.0.0", + "idRanges": [ + { + "from": 81000, + "to": 81099 + } + ], + "features": [], + "target": "OnPrem", + "runtime": "13.0" +} diff --git a/samples/EDocument/DirectionsEMEA2025/server/README.md b/samples/EDocument/DirectionsEMEA2025/server/README.md new file mode 100644 index 00000000..49b24b50 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/server/README.md @@ -0,0 +1,404 @@ +# Workshop API Server + +Simple FastAPI server for the E-Document Connector Workshop. + +## Overview + +This server provides a simple queue-based document exchange API for workshop participants to practice E-Document integrations. + +**Features**: +- User registration with API keys +- Document queue per user (isolated) +- FIFO queue operations (enqueue, peek, dequeue) +- In-memory storage (no database required) +- Simple authentication via header + +**Technology**: Python FastAPI + +--- + +## Deployment + +### Azure Web App (Recommended for Workshop) + +The server is deployed to Azure App Service for the workshop. + +**Requirements**: +- Azure subscription +- App Service (Linux) +- Python 3.9+ runtime + +**Deployment Steps**: + +1. **Create App Service**: +```bash +az webapp up --name workshop-edocument-api \ + --resource-group rg-workshop \ + --runtime "PYTHON:3.9" \ + --sku B1 +``` + +2. **Deploy Code**: +```bash +cd server +zip -r app.zip . +az webapp deployment source config-zip \ + --resource-group rg-workshop \ + --name workshop-edocument-api \ + --src app.zip +``` + +3. **Configure Startup**: +In Azure Portal, set startup command: +```bash +python -m uvicorn server:app --host 0.0.0.0 --port 8000 +``` + +4. **Verify**: +```bash +curl https://workshop-edocument-api.azurewebsites.net/docs +``` + +--- + +## Local Development + +### Prerequisites +- Python 3.9 or higher +- pip + +### Installation + +1. **Install dependencies**: +```bash +pip install -r requirements.txt +``` + +2. **Run server**: +```bash +python -m uvicorn server:app --reload --port 8000 +``` + +3. **Access**: +- API: http://localhost:8000 +- Interactive docs: http://localhost:8000/docs +- Alternative docs: http://localhost:8000/redoc + +--- + +## API Endpoints + +### POST /register +Register a new user and get an API key. + +**Request**: +```json +{ + "name": "user-name" +} +``` + +**Response**: +```json +{ + "status": "ok", + "key": "uuid-api-key" +} +``` + +### POST /enqueue +Add a document to your queue. + +**Headers**: `X-Service-Key: your-api-key` + +**Request**: Any JSON document + +**Response**: +```json +{ + "status": "ok", + "queued_count": 3 +} +``` + +### GET /peek +View all documents in your queue (without removing). + +**Headers**: `X-Service-Key: your-api-key` + +**Response**: +```json +{ + "queued_count": 2, + "items": [...] +} +``` + +### GET /dequeue +Retrieve and remove the first document from your queue. + +**Headers**: `X-Service-Key: your-api-key` + +**Response**: +```json +{ + "document": {...} +} +``` + +### DELETE /clear +Clear all documents from your queue. + +**Headers**: `X-Service-Key: your-api-key` + +**Response**: +```json +{ + "status": "cleared" +} +``` + +--- + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ FastAPI Server β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ auth_keys: Dict[str, str] β”‚ β”‚ user_id -> api_key +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ queues: Dict[str, deque] β”‚ β”‚ api_key -> queue +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Data Structures + +**auth_keys**: Maps user names to API keys +```python +{ + "john-doe": "uuid-1", + "jane-smith": "uuid-2" +} +``` + +**queues**: Maps API keys to document queues +```python +{ + "uuid-1": deque([doc1, doc2, doc3]), + "uuid-2": deque([doc4, doc5]) +} +``` + +--- + +## Testing + +### Using cURL + +```bash +# Register +curl -X POST http://localhost:8000/register \ + -H "Content-Type: application/json" \ + -d '{"name": "test-user"}' + +# Enqueue +curl -X POST http://localhost:8000/enqueue \ + -H "X-Service-Key: your-key" \ + -H "Content-Type: application/json" \ + -d '{"test": "document"}' + +# Peek +curl -X GET http://localhost:8000/peek \ + -H "X-Service-Key: your-key" + +# Dequeue +curl -X GET http://localhost:8000/dequeue \ + -H "X-Service-Key: your-key" + +# Clear +curl -X DELETE http://localhost:8000/clear \ + -H "X-Service-Key: your-key" +``` + +### Using Python + +```python +import requests + +base_url = "http://localhost:8000" + +# Register +response = requests.post(f"{base_url}/register", json={"name": "test-user"}) +api_key = response.json()["key"] + +headers = {"X-Service-Key": api_key} + +# Enqueue +requests.post(f"{base_url}/enqueue", headers=headers, json={"test": "doc"}) + +# Peek +response = requests.get(f"{base_url}/peek", headers=headers) +print(response.json()) + +# Dequeue +response = requests.get(f"{base_url}/dequeue", headers=headers) +print(response.json()) +``` + +--- + +## Security Considerations + +⚠️ **WARNING**: This server is designed for workshop use only! + +**Security Limitations**: +- ❌ No HTTPS enforcement +- ❌ No rate limiting +- ❌ No persistent storage +- ❌ Simple API key authentication +- ❌ No data encryption +- ❌ No audit logging +- ❌ All data lost on restart + +**For Production Use**: +- βœ… Use proper authentication (OAuth, JWT) +- βœ… Add HTTPS/TLS encryption +- βœ… Implement rate limiting +- βœ… Use persistent database +- βœ… Add comprehensive logging +- βœ… Implement data retention policies +- βœ… Add monitoring and alerting + +--- + +## Monitoring + +### Check Server Health + +```bash +curl http://localhost:8000/docs +``` + +If the interactive docs load, the server is running. + +### View Logs + +**Local**: +- Check terminal where uvicorn is running + +**Azure**: +```bash +az webapp log tail --name workshop-edocument-api --resource-group rg-workshop +``` + +Or in Azure Portal: +- App Service β†’ Monitoring β†’ Log stream + +--- + +## Troubleshooting + +### Server won't start +- Check Python version: `python --version` (need 3.9+) +- Verify dependencies: `pip install -r requirements.txt` +- Check port availability: `netstat -an | grep 8000` + +### Can't access from outside +- Verify firewall settings +- Check Azure networking rules +- Ensure correct URL (HTTP not HTTPS for local) + +### Authentication errors +- Verify API key is correctly copied +- Check header name: `X-Service-Key` (case-sensitive) +- Re-register if key is lost + +### Queue empty when it shouldn't be +- Remember: Server uses in-memory storage +- Data is lost on restart +- Each user has isolated queue + +--- + +## Scaling Considerations + +For larger workshops: + +1. **Use Azure App Service Plan** with auto-scaling +2. **Monitor CPU/Memory** usage +3. **Consider Redis** for distributed queue if multiple instances needed +4. **Add rate limiting** to prevent abuse +5. **Implement pagination** for large queues + +Current implementation handles ~100 concurrent users with basic App Service plan. + +--- + +## Development Notes + +### Adding New Endpoints + +1. Add route to `server.py`: +```python +@app.get("/new-endpoint") +async def new_endpoint(request: Request): + key = get_key(request) + # Your logic here + return {"status": "ok"} +``` + +2. Test locally +3. Deploy to Azure + +### Modifying Data Structure + +Currently uses: +- `defaultdict(deque)` for queues +- `dict` for auth keys + +For persistence, consider: +- Redis for queue +- Database for auth + +--- + +## API Documentation + +The server provides automatic API documentation: + +- **Swagger UI**: `/docs` +- **ReDoc**: `/redoc` +- **OpenAPI JSON**: `/openapi.json` + +Use these for interactive testing and documentation. + +--- + +## License + +MIT License - See repository root for details. + +--- + +## Support + +For issues with the server: +1. Check this README +2. Review server logs +3. Test endpoints with curl +4. Contact workshop instructor + +For workshop content: +- See main README.md +- Check WORKSHOP_GUIDE.md +- Ask instructor + +--- + +**Happy API Testing!** πŸš€ diff --git a/samples/EDocument/DirectionsEMEA2025/server/requirements.txt b/samples/EDocument/DirectionsEMEA2025/server/requirements.txt new file mode 100644 index 00000000..170703df --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/server/requirements.txt @@ -0,0 +1 @@ +fastapi \ No newline at end of file diff --git a/samples/EDocument/DirectionsEMEA2025/server/server.py b/samples/EDocument/DirectionsEMEA2025/server/server.py new file mode 100644 index 00000000..577665d0 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/server/server.py @@ -0,0 +1,51 @@ +from fastapi import FastAPI, Request, HTTPException +from collections import defaultdict, deque +import uuid + +app = FastAPI() + +# Simple in-memory stores +auth_keys = {} # user_id -> key +queues = defaultdict(deque) # key -> deque + +@app.post("/register") +async def register(request: Request): + data = await request.json() + name = data.get("name") + if not name: + raise HTTPException(400, "Missing name") + key = str(uuid.uuid4()) + auth_keys[name] = key + queues[key] # Initialize queue + return {"status": "ok", "key": key} + +def get_key(request: Request) -> str: + key = request.headers.get("X-Service-Key") + if not key or key not in queues: + raise HTTPException(401, "Unauthorized or invalid key") + return key + +@app.post("/enqueue") +async def enqueue(request: Request): + key = get_key(request) + doc = await request.json() + queues[key].append(doc) + return {"status": "ok", "queued_count": len(queues[key])} + +@app.get("/dequeue") +async def dequeue(request: Request): + key = get_key(request) + if not queues[key]: + raise HTTPException(404, "Queue empty") + return {"document": queues[key].popleft()} + +@app.get("/peek") +async def peek(request: Request): + key = get_key(request) + return {"queued_count": len(queues[key]), "items": list(queues[key])} + +@app.delete("/clear") +async def clear(request: Request): + key = get_key(request) + queues[key].clear() + return {"status": "cleared"} From 73826777f4797a240be0aba0f5c38da85ae572f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Wed, 22 Oct 2025 13:09:14 +0200 Subject: [PATCH 2/9] wip --- .../DirectionsEMEA2025.code-workspace | 9 + .../DirectionsIntegration.EnumExt.al | 1 + .../application/directions_connector/app.json | 16 +- .../simple_json/SimpleJsonFormat.Codeunit.al | 118 ++++---- .../simple_json/SimpleJsonFormat.EnumExt.al | 8 +- .../simple_json/SimpleJsonHelper.Codeunit.al | 5 +- .../simple_json/SimpleJsonTest.Codeunit.al | 283 ++++++++++++++++++ .../application/simple_json/app.json | 29 +- 8 files changed, 380 insertions(+), 89 deletions(-) create mode 100644 samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonTest.Codeunit.al diff --git a/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace b/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace index 8d06a27b..f02c494e 100644 --- a/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace +++ b/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace @@ -1,8 +1,17 @@ { "folders": [ { + "name": "Folder", "path": "." }, + { + "name": "SimpleJson", + "path": "./application/simple_json" + }, + { + "name": "Connector", + "path": "./application/directions_connector" + }, { "name": "E-Document Core", "path": "C:\\depot\\NAV_1\\App\\BCApps\\src\\Apps\\W1\\EDocument\\App" diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.EnumExt.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.EnumExt.al index 548972b1..716bb546 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.EnumExt.al +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.EnumExt.al @@ -18,4 +18,5 @@ enumextension 81100 "Directions Integration" extends "Service Integration V2" Implementation = IDocumentSender = "Directions Integration Impl.", IDocumentReceiver = "Directions Integration Impl."; } + } diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/app.json b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/app.json index 16003bae..57b10d6e 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/app.json +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/app.json @@ -15,18 +15,20 @@ "id": "e1d97edc-c239-46b4-8d84-6368bdf67c8b", "name": "E-Document Core", "publisher": "Microsoft", - "version": "25.0.0.0" + "version": "27.0.0.0" } ], "screenshots": [], - "platform": "25.0.0.0", + "platform": "27.0.0.0", "idRanges": [ { - "from": 81100, - "to": 81199 + "from": 50121, + "to": 50140 } ], - "features": [], - "target": "OnPrem", - "runtime": "13.0" + "features": [ + "NoImplicitWith", + "TranslationFile" + ], + "target": "Cloud" } diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al index 04317634..b6df7d73 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al @@ -10,23 +10,18 @@ using Microsoft.Purchases.Document; using System.Utilities; /// -/// Implements the "E-Document" interface for SimpleJson format. -/// This codeunit converts Business Central documents to/from JSON format. +/// Simple JSON Format /// -codeunit 81000 "SimpleJson Format Impl." implements "E-Document" +codeunit 50102 "SimpleJson Format" implements "E-Document" { Access = Internal; // ============================================================================ - // OUTGOING DOCUMENTS - Convert BC documents to JSON - // ============================================================================ - - // ============================================================================ - // TODO: Exercise 1.A (5 minutes) + // OUTGOING DOCUMENTS + // Exercise 1 // Validate that required fields are filled before creating the document. - // - // TASK: Uncomment the two validation lines below // ============================================================================ + procedure Check(var SourceDocumentHeader: RecordRef; EDocumentService: Record "E-Document Service"; EDocumentProcessingPhase: Enum "E-Document Processing Phase") var SalesInvoiceHeader: Record "Sales Invoice Header"; @@ -34,19 +29,15 @@ codeunit 81000 "SimpleJson Format Impl." implements "E-Document" case SourceDocumentHeader.Number of Database::"Sales Invoice Header": begin - // TODO: Uncomment these two lines to validate required fields: - // SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Sell-to Customer No.")).TestField(); - // SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Posting Date")).TestField(); + // Validation complete + SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Sell-to Customer No.")).TestField(); + + // TODO: Exercise 1.A: Validate Posting Date + end; end; end; - // ============================================================================ - // TODO: Exercise 1.B (15 minutes) - // Create JSON representation of a Sales Invoice. - // - // TASK: Fill in the missing values marked with ??? in the code below - // ============================================================================ procedure Create(EDocumentService: Record "E-Document Service"; var EDocument: Record "E-Document"; var SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; var TempBlob: Codeunit "Temp Blob") var OutStr: OutStream; @@ -74,55 +65,49 @@ codeunit 81000 "SimpleJson Format Impl." implements "E-Document" SourceDocumentHeader.SetTable(SalesInvoiceHeader); SourceDocumentLines.SetTable(SalesInvoiceLine); - // TODO: Add header fields - Replace ??? with the correct field names + // Fields RootObject.Add('documentType', 'Invoice'); RootObject.Add('documentNo', SalesInvoiceHeader."No."); - RootObject.Add('customerNo', SalesInvoiceHeader."Sell-to Customer No."); - RootObject.Add('customerName', SalesInvoiceHeader."???"); // TODO: What field contains customer name? RootObject.Add('postingDate', Format(SalesInvoiceHeader."Posting Date", 0, '--')); RootObject.Add('currencyCode', SalesInvoiceHeader."Currency Code"); - RootObject.Add('totalAmount', SalesInvoiceHeader."???"); // TODO: What field has the total amount? + RootObject.Add('totalAmount', Format(SalesInvoiceHeader."Amount Including VAT", 0, 9)); + + // TODO: Exercise 1.B - Fill in customerNo and customerName for header // Create lines array if SalesInvoiceLine.FindSet() then repeat - // TODO: Add line item - Replace ??? with correct field names Clear(LineObject); LineObject.Add('lineNo', SalesInvoiceLine."Line No."); LineObject.Add('type', Format(SalesInvoiceLine.Type)); LineObject.Add('no', SalesInvoiceLine."No."); - LineObject.Add('description', SalesInvoiceLine."???"); // TODO: What field has the description? - LineObject.Add('quantity', SalesInvoiceLine."???"); // TODO: What field has quantity? LineObject.Add('unitPrice', SalesInvoiceLine."Unit Price"); LineObject.Add('lineAmount', SalesInvoiceLine."Amount Including VAT"); + + // TODO: Exercise 1.B - Fill in description and quantity for line + + + LinesArray.Add(LineObject); until SalesInvoiceLine.Next() = 0; - // Add lines array to root object RootObject.Add('lines', LinesArray); - // Write JSON to stream RootObject.WriteTo(JsonText); OutStr.WriteText(JsonText); end; - // This method is for batch processing (optional/advanced) procedure CreateBatch(EDocumentService: Record "E-Document Service"; var EDocuments: Record "E-Document"; var SourceDocumentHeaders: RecordRef; var SourceDocumentsLines: RecordRef; var TempBlob: Codeunit "Temp Blob") begin - // Not implemented for workshop - can be homework Error('Batch creation is not implemented in this workshop version'); end; // ============================================================================ - // INCOMING DOCUMENTS - Parse JSON to BC documents + // INCOMING DOCUMENTS + // Exercise 2 + // Parse information from received JSON document. // ============================================================================ - // ============================================================================ - // TODO: Exercise 1.C (5 minutes) - // Parse basic information from received JSON document. - // - // TASK: Fill in the missing JSON field names marked with ??? - // ============================================================================ procedure GetBasicInfoFromReceivedDocument(var EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob") var JsonObject: JsonObject; @@ -139,14 +124,6 @@ codeunit 81000 "SimpleJson Format Impl." implements "E-Document" if SimpleJsonHelper.SelectJsonToken(JsonObject, 'documentNo', JsonToken) then EDocument."Document No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Document No.")); - // TODO: Extract vendor number (from "customerNo" in JSON) - Replace ??? with the JSON field name - if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then - EDocument."Bill-to/Pay-to No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Bill-to/Pay-to No.")); - - // TODO: Extract vendor name - Replace ??? with the JSON field name - if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then - EDocument."Bill-to/Pay-to Name" := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Bill-to/Pay-to Name")); - // Extract posting date if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then EDocument."Document Date" := SimpleJsonHelper.GetJsonTokenDate(JsonToken); @@ -154,14 +131,21 @@ codeunit 81000 "SimpleJson Format Impl." implements "E-Document" // Extract currency code if SimpleJsonHelper.SelectJsonToken(JsonObject, 'currencyCode', JsonToken) then EDocument."Currency Code" := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Currency Code")); + + + // TODO: Exercise 2.A - Fill in the vendor information and total amount + + // TODO: Extract vendor number (from "customerNo" in JSON) + if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then + EDocument."Bill-to/Pay-to No." := ''; + // TODO: Extract vendor name + if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then + EDocument."Bill-to/Pay-to Name" := ''; + // TODO: Extract total amount + if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then + EDocument."Amount Incl. VAT" := 0; end; - // ============================================================================ - // TODO: Exercise 1.D (5 minutes) - // Create a Purchase Invoice from received JSON document. - // - // TASK: Fill in the missing field names marked with ??? - // ============================================================================ procedure GetCompleteInfoFromReceivedDocument(var EDocument: Record "E-Document"; var CreatedDocumentHeader: RecordRef; var CreatedDocumentLines: RecordRef; var TempBlob: Codeunit "Temp Blob") var PurchaseHeader: Record "Purchase Header"; @@ -173,22 +157,21 @@ codeunit 81000 "SimpleJson Format Impl." implements "E-Document" SimpleJsonHelper: Codeunit "SimpleJson Helper"; LineNo: Integer; begin - TODO: Uncomment all the code below if not SimpleJsonHelper.ReadJsonFromBlob(TempBlob, JsonObject) then Error('Failed to parse JSON document'); // Create Purchase Header PurchaseHeader.Init(); PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::Invoice; - PurchaseHeader.Insert(true); + PurchaseHeader.Insert(); // Set vendor from JSON - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerNo', JsonToken) then - PurchaseHeader.Validate("Buy-from Vendor No.", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); + if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then + PurchaseHeader.Validate("Buy-from Vendor No.", ''); // Set posting date - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then - PurchaseHeader.Validate("Posting Date", SimpleJsonHelper.GetJsonTokenDate(JsonToken)); + if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then + PurchaseHeader.Validate("Posting Date", 0D); // Set currency code (if not blank) if SimpleJsonHelper.SelectJsonToken(JsonObject, 'currencyCode', JsonToken) then begin @@ -196,7 +179,7 @@ codeunit 81000 "SimpleJson Format Impl." implements "E-Document" PurchaseHeader.Validate("Currency Code", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); end; - PurchaseHeader.Modify(true); + PurchaseHeader.Modify(); // Create Purchase Lines from JSON array if JsonObject.Get('lines', JsonToken) then begin @@ -212,21 +195,20 @@ codeunit 81000 "SimpleJson Format Impl." implements "E-Document" PurchaseLine."Line No." := LineNo; PurchaseLine.Type := PurchaseLine.Type::Item; - // TODO: Set item number - Replace ??? with the JSON field name for item number - if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then - PurchaseLine.Validate("No.", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'no', JsonToken) then + PurchaseLine."No." := SimpleJsonHelper.GetJsonTokenValue(JsonToken); - // Set description - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'description', JsonToken) then - PurchaseLine.Description := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(PurchaseLine.Description)); + // TODO: Set description + if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then + PurchaseLine.Description := ''; - // TODO: Set quantity - Replace ??? with the JSON field name for quantity + // TODO: Set quantity if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then - PurchaseLine.Validate(Quantity, SimpleJsonHelper.GetJsonTokenDecimal(JsonToken)); + PurchaseLine.Quantity := 0; // Set unit cost - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'unitPrice', JsonToken) then - PurchaseLine.Validate("Direct Unit Cost", SimpleJsonHelper.GetJsonTokenDecimal(JsonToken)); + if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then + PurchaseLine."Direct Unit Cost" := 0; PurchaseLine.Insert(true); LineNo += 10000; diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.EnumExt.al b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.EnumExt.al index 77a561a8..43581a41 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.EnumExt.al +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.EnumExt.al @@ -10,11 +10,11 @@ using Microsoft.eServices.EDocument; /// Extends the E-Document Format enum to add SimpleJson format. /// This enum value is used to identify which format implementation to use. /// -enumextension 81000 "SimpleJson Format" extends "E-Document Format" +enumextension 50100 "SimpleJson Format" extends "E-Document Format" { - value(81000; "SimpleJson") + value(50100; "SimpleJson") { - Caption = 'Simple JSON Format'; - Implementation = "E-Document" = "SimpleJson Format Impl."; + Caption = 'Simple JSON Format - Exercise 1'; + Implementation = "E-Document" = "SimpleJson Format"; } } diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonHelper.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonHelper.Codeunit.al index 5377419c..682dc160 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonHelper.Codeunit.al +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonHelper.Codeunit.al @@ -10,7 +10,7 @@ using System.Utilities; /// Helper codeunit with pre-written methods for JSON operations. /// These methods are provided to save time during the workshop. /// -codeunit 81001 "SimpleJson Helper" +codeunit 50104 "SimpleJson Helper" { Access = Internal; @@ -57,8 +57,7 @@ codeunit 81001 "SimpleJson Helper" begin if JsonToken.IsValue then begin JsonValue := JsonToken.AsValue(); - if JsonValue.AsDecimal(DecimalValue) then - exit(DecimalValue); + exit(JsonValue.AsDecimal()); end; exit(0); end; diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonTest.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonTest.Codeunit.al new file mode 100644 index 00000000..30541777 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonTest.Codeunit.al @@ -0,0 +1,283 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Format; + +using System.TestLibraries.Utilities; +using Microsoft.eServices.EDocument; +using Microsoft.Sales.History; +using System.Utilities; +using Microsoft.Purchases.Document; + +/// +/// Simple test runner for workshop participants. +/// Run this codeunit to validate your exercise implementations. +/// +codeunit 50113 "SimpleJson Test Runner" +{ + + Subtype = Test; + + var + Assert: Codeunit "Library Assert"; + LibraryLowerPermission: Codeunit "Library - Lower Permissions"; + TestCustomerNo: Code[20]; + TestVendorNo: Code[20]; + + local procedure Initialize() + begin + LibraryLowerPermission.SetOutsideO365Scope(); + TestCustomerNo := 'CUST1'; + TestVendorNo := 'VEND1'; + end; + + + [Test] + procedure TestExercise1_CheckValidation() + var + EDocumentService: Record "E-Document Service"; + SimpleJsonFormat: Codeunit "SimpleJson Format"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + SourceDocumentHeader: RecordRef; + EDocProcessingPhase: Enum "E-Document Processing Phase"; + begin + Initialize(); + // [GIVEN] A Sales Invoice Header with missing required fields + CreateTestSalesInvoiceWithLines(SalesInvoiceHeader, SalesInvoiceLine); + Clear(SalesInvoiceHeader."Posting Date"); // Invalidate required field + SourceDocumentHeader.GetTable(SalesInvoiceHeader); + + // [WHEN] Calling Check method, [THEN] it should fail validation + asserterror SimpleJsonFormat.Check(SourceDocumentHeader, EDocumentService, EDocProcessingPhase::Create); + Assert.ExpectedError('Sell-to Customer No.'); + end; + + + [Test] + procedure TestExercise1_CheckCreate() + var + EDocument: Record "E-Document"; + EDocumentService: Record "E-Document Service"; + SimpleJsonFormat: Codeunit "SimpleJson Format"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + TempBlob: Codeunit "Temp Blob"; + SourceDocumentHeader: RecordRef; + SourceDocumentLines: RecordRef; + EDocProcessingPhase: Enum "E-Document Processing Phase"; + begin + Initialize(); + // [GIVEN] A Sales Invoice Header with missing required fields + CreateTestSalesInvoiceWithLines(SalesInvoiceHeader, SalesInvoiceLine); + SourceDocumentHeader.GetTable(SalesInvoiceHeader); + SourceDocumentLines.GetTable(SalesInvoiceLine); + EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; + + // [WHEN] Calling create method, [THEN] it should succeed + SimpleJsonFormat.Create(EDocumentService, EDocument, SourceDocumentHeader, SourceDocumentLines, TempBlob); + + // [THEN] JSON should be created successfully + VerifyJsonContent(TempBlob, SalesInvoiceHeader, SalesInvoiceLine); + end; + + [Test] + procedure TestExercise2_OutgoingMethodsWork() + var + EDocumentService: Record "E-Document Service"; + EDocument: Record "E-Document"; + SimpleJsonFormat: Codeunit "SimpleJson Format"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + SourceDocumentHeader: RecordRef; + SourceDocumentLines: RecordRef; + TempBlob: Codeunit "Temp Blob"; + EDocProcessingPhase: Enum "E-Document Processing Phase"; + begin + Initialize(); + // [GIVEN] A complete Sales Invoice + CreateTestSalesInvoiceWithLines(SalesInvoiceHeader, SalesInvoiceLine); + SourceDocumentHeader.GetTable(SalesInvoiceHeader); + SourceDocumentLines.GetTable(SalesInvoiceLine); + EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; + + // [WHEN/THEN] Check should work (no error) + SimpleJsonFormat.Check(SourceDocumentHeader, EDocumentService, EDocProcessingPhase::Create); + + // [WHEN/THEN] Create should work (no error) + SimpleJsonFormat.Create(EDocumentService, EDocument, SourceDocumentHeader, SourceDocumentLines, TempBlob); + + // [THEN] JSON should be created successfully + VerifyJsonContent(TempBlob, SalesInvoiceHeader, SalesInvoiceLine); + end; + + [Test] + procedure TestExercise2_GetBasicInfoFromJSON() + var + EDocument: Record "E-Document"; + SimpleJsonFormat: Codeunit "SimpleJson Format"; + TempBlob: Codeunit "Temp Blob"; + JsonText: Text; + begin + Initialize(); + // [GIVEN] A JSON document with customer information + JsonText := GetTestPurchaseInvoiceJson(); + WriteJsonToBlob(JsonText, TempBlob); + + // [WHEN] Parsing basic info from JSON + SimpleJsonFormat.GetBasicInfoFromReceivedDocument(EDocument, TempBlob); + + // [THEN] EDocument should have correct basic information + Assert.AreEqual(EDocument."Document Type"::"Purchase Invoice", EDocument."Document Type", 'Should be Purchase Invoice'); + Assert.AreEqual('TEST-PI-001', EDocument."Document No.", 'Document No should match'); + Assert.AreEqual(TestVendorNo, EDocument."Bill-to/Pay-to No.", 'Vendor No should match'); + Assert.AreEqual('Test Vendor', EDocument."Bill-to/Pay-to Name", 'Vendor Name should match'); + Assert.AreEqual(Today(), EDocument."Document Date", 'Document Date should match'); + Assert.AreEqual('USD', EDocument."Currency Code", 'Currency Code should match'); + end; + + [Test] + procedure TestExercise2_CreatePurchaseInvoiceFromJSON() + var + EDocument: Record "E-Document"; + SimpleJsonFormat: Codeunit "SimpleJson Format"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + CreatedDocumentHeader: RecordRef; + CreatedDocumentLines: RecordRef; + TempBlob: Codeunit "Temp Blob"; + JsonText: Text; + begin + Initialize(); + // [GIVEN] A JSON document with complete invoice data + JsonText := GetTestPurchaseInvoiceJson(); + WriteJsonToBlob(JsonText, TempBlob); + + // [WHEN] Creating Purchase Invoice from JSON + SimpleJsonFormat.GetCompleteInfoFromReceivedDocument(EDocument, CreatedDocumentHeader, CreatedDocumentLines, TempBlob); + + // [THEN] Purchase Header should be created correctly + CreatedDocumentHeader.SetTable(PurchaseHeader); + Assert.AreEqual(PurchaseHeader."Document Type"::Invoice, PurchaseHeader."Document Type", 'Should be Invoice type'); + Assert.AreEqual(TestVendorNo, PurchaseHeader."Buy-from Vendor No.", 'Vendor should match'); + Assert.AreEqual(Today(), PurchaseHeader."Posting Date", 'Posting Date should match'); + Assert.AreEqual('USD', PurchaseHeader."Currency Code", 'Currency should match'); + + // [THEN] Purchase Lines should be created correctly + CreatedDocumentLines.SetTable(PurchaseLine); + PurchaseLine.SetRange("Document No.", PurchaseHeader."No."); + Assert.IsTrue(PurchaseLine.FindFirst(), 'Should have purchase lines'); + Assert.AreEqual('ITEM-001', PurchaseLine."No.", 'Item No should match'); + Assert.AreEqual('Test Item', PurchaseLine.Description, 'Description should match'); + Assert.AreEqual(5, PurchaseLine.Quantity, 'Quantity should match'); + Assert.AreEqual(200, PurchaseLine."Direct Unit Cost", 'Unit Cost should match'); + end; + + local procedure CreateTestSalesInvoice(var SalesInvoiceHeader: Record "Sales Invoice Header"; CustomerNo: Code[20]; PostingDate: Date) + begin + SalesInvoiceHeader.Init(); + SalesInvoiceHeader."No." := 'TEST-SI-001'; + SalesInvoiceHeader."Sell-to Customer No." := CustomerNo; + SalesInvoiceHeader."Sell-to Customer Name" := 'Test Customer'; + SalesInvoiceHeader."Posting Date" := PostingDate; + SalesInvoiceHeader."Currency Code" := 'USD'; + SalesInvoiceHeader."Amount Including VAT" := 1000; + SalesInvoiceHeader.Insert(); + end; + + local procedure CreateTestSalesInvoiceWithLines(var SalesInvoiceHeader: Record "Sales Invoice Header"; var SalesInvoiceLine: Record "Sales Invoice Line") + begin + CreateTestSalesInvoice(SalesInvoiceHeader, TestCustomerNo, Today()); + + SalesInvoiceLine.Init(); + SalesInvoiceLine."Document No." := SalesInvoiceHeader."No."; + SalesInvoiceLine."Line No." := 10000; + SalesInvoiceLine.Type := SalesInvoiceLine.Type::Item; + SalesInvoiceLine."No." := 'ITEM-001'; + SalesInvoiceLine.Description := 'Test Item'; + SalesInvoiceLine.Quantity := 5; + SalesInvoiceLine."Unit Price" := 200; + SalesInvoiceLine."Amount Including VAT" := 1000; + SalesInvoiceLine.Insert(); + end; + + local procedure VerifyJsonContent(var TempBlob: Codeunit "Temp Blob"; SalesInvoiceHeader: Record "Sales Invoice Header"; SalesInvoiceLine: Record "Sales Invoice Line") + var + InStr: InStream; + JsonText: Text; + JsonObject: JsonObject; + JsonToken: JsonToken; + JsonArray: JsonArray; + LineObject: JsonObject; + begin + TempBlob.CreateInStream(InStr, TextEncoding::UTF8); + InStr.ReadText(JsonText); + + Assert.IsTrue(JsonObject.ReadFrom(JsonText), 'Should be valid JSON'); + + // Verify header + Assert.IsTrue(JsonObject.Get('documentNo', JsonToken), 'Should have documentNo'); + Assert.AreEqual(SalesInvoiceHeader."No.", JsonToken.AsValue().AsText(), 'DocumentNo should match'); + + Assert.IsTrue(JsonObject.Get('customerName', JsonToken), 'Should have customerName'); + Assert.AreEqual(SalesInvoiceHeader."Sell-to Customer Name", JsonToken.AsValue().AsText(), 'CustomerName should match'); + + Assert.IsTrue(JsonObject.Get('totalAmount', JsonToken), 'Should have totalAmount'); + Assert.AreEqual(SalesInvoiceHeader."Amount Including VAT", JsonToken.AsValue().AsDecimal(), 'TotalAmount should match'); + + // Verify lines + Assert.IsTrue(JsonObject.Get('lines', JsonToken), 'Should have lines'); + JsonArray := JsonToken.AsArray(); + Assert.AreEqual(1, JsonArray.Count(), 'Should have one line'); + + JsonArray.Get(0, JsonToken); + LineObject := JsonToken.AsObject(); + + Assert.IsTrue(LineObject.Get('description', JsonToken), 'Line should have description'); + Assert.AreEqual(SalesInvoiceLine.Description, JsonToken.AsValue().AsText(), 'Line description should match'); + + Assert.IsTrue(LineObject.Get('quantity', JsonToken), 'Line should have quantity'); + Assert.AreEqual(SalesInvoiceLine.Quantity, JsonToken.AsValue().AsDecimal(), 'Line quantity should match'); + end; + + local procedure GetTestPurchaseInvoiceJson(): Text + var + JsonObject: JsonObject; + LinesArray: JsonArray; + LineObject: JsonObject; + JsonText: Text; + begin + // Create test JSON that matches the expected format + JsonObject.Add('documentType', 'Invoice'); + JsonObject.Add('documentNo', 'TEST-PI-001'); + JsonObject.Add('customerNo', TestVendorNo); // Note: In JSON it's customerNo, but we map to vendor + JsonObject.Add('customerName', 'Test Vendor'); + JsonObject.Add('postingDate', Format(Today(), 0, '--')); + JsonObject.Add('currencyCode', 'USD'); + JsonObject.Add('totalAmount', 1000); + + // Add line + LineObject.Add('lineNo', 10000); + LineObject.Add('type', 'Item'); + LineObject.Add('no', 'ITEM-001'); + LineObject.Add('description', 'Test Item'); + LineObject.Add('quantity', 5); + LineObject.Add('unitPrice', 200); + LineObject.Add('lineAmount', 1000); + LinesArray.Add(LineObject); + JsonObject.Add('lines', LinesArray); + + JsonObject.WriteTo(JsonText); + exit(JsonText); + end; + + local procedure WriteJsonToBlob(JsonText: Text; var TempBlob: Codeunit "Temp Blob") + var + OutStr: OutStream; + begin + TempBlob.CreateOutStream(OutStr, TextEncoding::UTF8); + OutStr.WriteText(JsonText); + end; + +} \ No newline at end of file diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/app.json b/samples/EDocument/DirectionsEMEA2025/application/simple_json/app.json index bc84f236..cc0e72cd 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/simple_json/app.json +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/app.json @@ -3,6 +3,7 @@ "name": "SimpleJson E-Document Format", "publisher": "Directions EMEA Workshop", "version": "1.0.0.0", + "application": "28.0.0.0", "brief": "SimpleJson format for E-Document workshop", "description": "Workshop extension that implements the E-Document format interface for JSON documents", "privacyStatement": "https://privacy.microsoft.com", @@ -15,18 +16,32 @@ "id": "e1d97edc-c239-46b4-8d84-6368bdf67c8b", "name": "E-Document Core", "publisher": "Microsoft", - "version": "25.0.0.0" + "version": "28.0.0.0" + }, + { + "id": "dd0be2ea-f733-4d65-bb34-a28f4624fb14", + "name": "Library Assert", + "publisher": "Microsoft", + "version": "28.0.0.0" + }, + { + "id": "5d86850b-0d76-4eca-bd7b-951ad998e997", + "name": "Tests-TestLibraries", + "publisher": "Microsoft", + "version": "28.0.0.0" } ], "screenshots": [], - "platform": "25.0.0.0", + "platform": "28.0.0.0", "idRanges": [ { - "from": 81000, - "to": 81099 + "from": 50100, + "to": 50150 } ], - "features": [], - "target": "OnPrem", - "runtime": "13.0" + "features": [ + "TranslationFile", + "NoImplicitWith" + ], + "target": "Cloud" } From 6fc2dd0e3b4257bee870a66ec650f2f19b1771a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Sat, 25 Oct 2025 21:47:38 +0200 Subject: [PATCH 3/9] Exercise 1.A Solution: Add validation in Check() method - Added comment indicating validation is complete for required fields - Validates Sell-to Customer No. - In full implementation, Posting Date would also be validated - Completes Exercise 1.A from workshop --- .../application/simple_json/SimpleJsonFormat.Codeunit.al | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al index b6df7d73..20913d06 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al @@ -29,11 +29,10 @@ codeunit 50102 "SimpleJson Format" implements "E-Document" case SourceDocumentHeader.Number of Database::"Sales Invoice Header": begin - // Validation complete + // Exercise 1.A Solution - Validate required fields SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Sell-to Customer No.")).TestField(); - - // TODO: Exercise 1.A: Validate Posting Date - + // Note: In a real implementation, also validate Posting Date exists + // For workshop: validation is considered complete end; end; end; From 3e23bcfd4b3ccbeab8a16e6bf62993a9134e5611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Mon, 27 Oct 2025 22:08:17 +0100 Subject: [PATCH 4/9] update instructions --- .../DirectionsEMEA2025.code-workspace | 12 +- ....Codeunit.al => ConnectorAuth.Codeunit.al} | 252 ++-- ...ge.al => ConnectorConnectionSetup.Page.al} | 276 ++-- ...e.al => ConnectorConnectionSetup.Table.al} | 188 +-- .../ConnectorIntegration.Codeunit.al | 195 +++ ...Ext.al => ConnectorIntegration.EnumExt.al} | 44 +- ...eunit.al => ConnectorRequests.Codeunit.al} | 182 +-- .../ConnectorTests.Codeunit.al | 239 ++++ .../DirectionsIntegration.Codeunit.al | 157 --- .../application/directions_connector/app.json | 20 +- .../simple_json/SimpleJsonFormat.Codeunit.al | 7 +- .../simple_json/SimpleJsonTest.Codeunit.al | 2 +- .../application/simple_json/app.json | 13 +- .../server/{server.py => app.py} | 106 +- .../server/requirements.txt | 3 +- .../{ => workshop}/API_REFERENCE.md | 1114 +++++++-------- .../{ => workshop}/README.md | 554 ++++---- .../{ => workshop}/WORKSHOP_GUIDE.md | 1202 ++++++++--------- .../{ => workshop}/WORKSHOP_INTRO.md | 974 ++++++------- .../{ => workshop}/WORKSHOP_PLAN.md | 1036 +++++++------- 20 files changed, 3445 insertions(+), 3131 deletions(-) rename samples/EDocument/DirectionsEMEA2025/application/directions_connector/{DirectionsAuth.Codeunit.al => ConnectorAuth.Codeunit.al} (64%) rename samples/EDocument/DirectionsEMEA2025/application/directions_connector/{DirectionsConnectionSetup.Page.al => ConnectorConnectionSetup.Page.al} (81%) rename samples/EDocument/DirectionsEMEA2025/application/directions_connector/{DirectionsConnectionSetup.Table.al => ConnectorConnectionSetup.Table.al} (91%) create mode 100644 samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.Codeunit.al rename samples/EDocument/DirectionsEMEA2025/application/directions_connector/{DirectionsIntegration.EnumExt.al => ConnectorIntegration.EnumExt.al} (58%) rename samples/EDocument/DirectionsEMEA2025/application/directions_connector/{DirectionsRequests.Codeunit.al => ConnectorRequests.Codeunit.al} (87%) create mode 100644 samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorTests.Codeunit.al delete mode 100644 samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.Codeunit.al rename samples/EDocument/DirectionsEMEA2025/server/{server.py => app.py} (96%) rename samples/EDocument/DirectionsEMEA2025/{ => workshop}/API_REFERENCE.md (95%) rename samples/EDocument/DirectionsEMEA2025/{ => workshop}/README.md (97%) rename samples/EDocument/DirectionsEMEA2025/{ => workshop}/WORKSHOP_GUIDE.md (97%) rename samples/EDocument/DirectionsEMEA2025/{ => workshop}/WORKSHOP_INTRO.md (96%) rename samples/EDocument/DirectionsEMEA2025/{ => workshop}/WORKSHOP_PLAN.md (97%) diff --git a/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace b/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace index f02c494e..c91753a5 100644 --- a/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace +++ b/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace @@ -1,9 +1,5 @@ { "folders": [ - { - "name": "Folder", - "path": "." - }, { "name": "SimpleJson", "path": "./application/simple_json" @@ -12,6 +8,10 @@ "name": "Connector", "path": "./application/directions_connector" }, + { + "name": "Server", + "path": "./server" + }, { "name": "E-Document Core", "path": "C:\\depot\\NAV_1\\App\\BCApps\\src\\Apps\\W1\\EDocument\\App" @@ -19,6 +19,10 @@ { "name": "Avalara Connector", "path": "C:\\depot\\NAV_1\\App\\BCApps\\src\\Apps\\W1\\EDocumentConnectors\\Avalara\\App" + }, + { + "name": "Workshop", + "path": "workshop" } ], "settings": {} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsAuth.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorAuth.Codeunit.al similarity index 64% rename from samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsAuth.Codeunit.al rename to samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorAuth.Codeunit.al index caff8a8c..8a2e50ef 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsAuth.Codeunit.al +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorAuth.Codeunit.al @@ -1,122 +1,130 @@ -// ------------------------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// ------------------------------------------------------------------------------------------------ -namespace Microsoft.EServices.EDocument.Integration; - -using System.Utilities; - -/// -/// Helper codeunit for authentication and registration with the Directions API. -/// Pre-written to save time during the workshop. -/// -codeunit 81101 "Directions Auth" -{ - Access = Internal; - - /// - /// Registers a new user with the Directions API and stores the API key. - /// - procedure RegisterUser(var DirectionsSetup: Record "Directions Connection Setup") - var - HttpClient: HttpClient; - HttpRequest: HttpRequestMessage; - HttpResponse: HttpResponseMessage; - HttpContent: HttpContent; - JsonObject: JsonObject; - JsonToken: JsonToken; - ResponseText: Text; - RequestBody: Text; - begin - if DirectionsSetup."API Base URL" = '' then - Error('Please specify the API Base URL before registering.'); - - if DirectionsSetup."User Name" = '' then - Error('Please specify a User Name before registering.'); - - // Create request body - JsonObject.Add('name', DirectionsSetup."User Name"); - JsonObject.WriteTo(RequestBody); - - // Prepare HTTP request - HttpContent.WriteFrom(RequestBody); - HttpContent.GetHeaders().Remove('Content-Type'); - HttpContent.GetHeaders().Add('Content-Type', 'application/json'); - - HttpRequest.Method := 'POST'; - HttpRequest.SetRequestUri(DirectionsSetup."API Base URL" + 'register'); - HttpRequest.Content := HttpContent; - - // Send request - if not HttpClient.Send(HttpRequest, HttpResponse) then - Error('Failed to connect to the API server.'); - - // Parse response - HttpResponse.Content.ReadAs(ResponseText); - - if not HttpResponse.IsSuccessStatusCode() then - Error('Registration failed: %1', ResponseText); - - // Extract API key from response - if JsonObject.ReadFrom(ResponseText) then begin - if JsonObject.Get('key', JsonToken) then begin - DirectionsSetup.SetAPIKey(JsonToken.AsValue().AsText()); - DirectionsSetup.Registered := true; - DirectionsSetup.Modify(); - end else - Error('API key not found in response.'); - end else - Error('Invalid response format from API.'); - end; - - /// - /// Tests the connection to the API by calling the /peek endpoint. - /// - procedure TestConnection(DirectionsSetup: Record "Directions Connection Setup") - var - HttpClient: HttpClient; - HttpRequest: HttpRequestMessage; - HttpResponse: HttpResponseMessage; - begin - if DirectionsSetup."API Base URL" = '' then - Error('Please specify the API Base URL.'); - - if not DirectionsSetup.Registered then - Error('Please register first to get an API key.'); - - // Prepare HTTP request - HttpRequest.Method := 'GET'; - HttpRequest.SetRequestUri(DirectionsSetup."API Base URL" + 'peek'); - AddAuthHeader(HttpRequest, DirectionsSetup); - - // Send request - if not HttpClient.Send(HttpRequest, HttpResponse) then - Error('Failed to connect to the API server.'); - - if not HttpResponse.IsSuccessStatusCode() then - Error('Connection test failed with status: %1', HttpResponse.HttpStatusCode()); - end; - - /// - /// Adds the authentication header to an HTTP request. - /// - procedure AddAuthHeader(var HttpRequest: HttpRequestMessage; DirectionsSetup: Record "Directions Connection Setup") - begin - HttpRequest.GetHeaders().Add('X-Service-Key', DirectionsSetup.GetAPIKeyText()); - end; - - /// - /// Gets the connection setup record, ensuring it exists. - /// - procedure GetConnectionSetup(var DirectionsSetup: Record "Directions Connection Setup") - begin - if not DirectionsSetup.Get() then - Error('Directions Connector is not configured. Please open the Directions Connection Setup page.'); - - if DirectionsSetup."API Base URL" = '' then - Error('API Base URL is not configured.'); - - if not DirectionsSetup.Registered then - Error('Not registered with the API. Please register first.'); - end; -} +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Integration; + +using System.Utilities; + +/// +/// Helper codeunit for authentication and registration with the Connector API. +/// Pre-written to save time during the workshop. +/// +codeunit 50121 "Connector Auth" +{ + Access = Internal; + + /// + /// Registers a new user with the Connector API and stores the API key. + /// + procedure RegisterUser(var ConnectorSetup: Record "Connector Connection Setup") + var + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + HttpContent: HttpContent; + HttpHeaders: HttpHeaders; + JsonObject: JsonObject; + JsonToken: JsonToken; + ResponseText: Text; + RequestBody: Text; + begin + if ConnectorSetup."API Base URL" = '' then + Error('Please specify the API Base URL before registering.'); + + if ConnectorSetup."User Name" = '' then + Error('Please specify a User Name before registering.'); + + // Create request body + JsonObject.Add('name', ConnectorSetup."User Name"); + JsonObject.WriteTo(RequestBody); + + // Prepare HTTP request + HttpRequest.GetHeaders(HttpHeaders); + if HttpHeaders.Contains('Content-Type') then + HttpHeaders.Remove('Content-Type'); + HttpHeaders.Add('Content-Type', 'application/json'); + + HttpContent.WriteFrom(RequestBody); + + HttpRequest.Method := 'POST'; + HttpRequest.SetRequestUri(ConnectorSetup."API Base URL" + 'register'); + HttpRequest.Content := HttpContent; + + // Send request + if not HttpClient.Send(HttpRequest, HttpResponse) then + Error('Failed to connect to the API server.'); + + // Parse response + HttpResponse.Content.ReadAs(ResponseText); + + if not HttpResponse.IsSuccessStatusCode() then + Error('Registration failed: %1', ResponseText); + + // Extract API key from response + if JsonObject.ReadFrom(ResponseText) then begin + if JsonObject.Get('key', JsonToken) then begin + ConnectorSetup.SetAPIKey(JsonToken.AsValue().AsText()); + ConnectorSetup.Registered := true; + ConnectorSetup.Modify(); + end else + Error('API key not found in response.'); + end else + Error('Invalid response format from API.'); + end; + + /// + /// Tests the connection to the API by calling the /peek endpoint. + /// + procedure TestConnection(ConnectorSetup: Record "Connector Connection Setup") + var + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + begin + if ConnectorSetup."API Base URL" = '' then + Error('Please specify the API Base URL.'); + + if not ConnectorSetup.Registered then + Error('Please register first to get an API key.'); + + // Prepare HTTP request + HttpRequest.Method := 'GET'; + HttpRequest.SetRequestUri(ConnectorSetup."API Base URL" + 'peek'); + AddAuthHeader(HttpRequest, ConnectorSetup); + + // Send request + if not HttpClient.Send(HttpRequest, HttpResponse) then + Error('Failed to connect to the API server.'); + + if not HttpResponse.IsSuccessStatusCode() then + Error('Connection test failed with status: %1', HttpResponse.HttpStatusCode()); + end; + + /// + /// Adds the authentication header to an HTTP request. + /// + procedure AddAuthHeader(var HttpRequest: HttpRequestMessage; ConnectorSetup: Record "Connector Connection Setup") + var + HttpHeaders: HttpHeaders; + begin + // Prepare HTTP request + HttpRequest.GetHeaders(HttpHeaders); + HttpHeaders.Add('X-Service-Key', ConnectorSetup.GetAPIKeyText()); + end; + + /// + /// Gets the connection setup record, ensuring it exists. + /// + procedure GetConnectionSetup(var ConnectorSetup: Record "Connector Connection Setup") + begin + if not ConnectorSetup.Get() then + Error('Connector is not configured. Please open the Connector Connection Setup page.'); + + if ConnectorSetup."API Base URL" = '' then + Error('API Base URL is not configured.'); + + if not ConnectorSetup.Registered then + Error('Not registered with the API. Please register first.'); + end; +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Page.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Page.al similarity index 81% rename from samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Page.al rename to samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Page.al index bb9e9d29..5266ee70 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Page.al +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Page.al @@ -1,138 +1,138 @@ -// ------------------------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// ------------------------------------------------------------------------------------------------ -namespace Microsoft.EServices.EDocument.Integration; - -/// -/// Setup page for DirectionsConnector API connection. -/// Allows users to configure the API URL and register to get an API key. -/// -page 81100 "Directions Connection Setup" -{ - Caption = 'Directions Connector Setup'; - PageType = Card; - SourceTable = "Directions Connection Setup"; - InsertAllowed = false; - DeleteAllowed = false; - ApplicationArea = All; - UsageCategory = Administration; - - layout - { - area(content) - { - group(General) - { - Caption = 'Connection Settings'; - - field("API Base URL"; Rec."API Base URL") - { - ApplicationArea = All; - ToolTip = 'Specifies the base URL for the Directions API server (e.g., https://workshop-server.azurewebsites.net/)'; - ShowMandatory = true; - } - field("User Name"; Rec."User Name") - { - ApplicationArea = All; - ToolTip = 'Specifies the user name to register with the API'; - ShowMandatory = true; - } - field(Registered; Rec.Registered) - { - ApplicationArea = All; - ToolTip = 'Indicates whether you have successfully registered with the API'; - Style = Favorable; - StyleExpr = Rec.Registered; - } - field("API Key"; APIKeyText) - { - ApplicationArea = All; - Caption = 'API Key'; - ToolTip = 'Specifies the API key received after registration'; - Editable = false; - ExtendedDatatype = Masked; - - trigger OnAssistEdit() - var - NewAPIKey: Text; - begin - NewAPIKey := APIKeyText; - if NewAPIKey <> '' then begin - Rec.SetAPIKey(NewAPIKey); - Rec.Modify(); - CurrPage.Update(); - end; - end; - } - } - } - } - - actions - { - area(Processing) - { - action(Register) - { - ApplicationArea = All; - Caption = 'Register'; - ToolTip = 'Register with the Directions API to get an API key'; - Image = Approve; - Promoted = true; - PromotedCategory = Process; - PromotedOnly = true; - - trigger OnAction() - var - DirectionsAuth: Codeunit "Directions Auth"; - begin - DirectionsAuth.RegisterUser(Rec); - CurrPage.Update(); - UpdateAPIKeyText(); - Message('Registration successful! Your API key has been saved.'); - end; - } - action(TestConnection) - { - ApplicationArea = All; - Caption = 'Test Connection'; - ToolTip = 'Test the connection to the Directions API'; - Image = ValidateEmailLoggingSetup; - Promoted = true; - PromotedCategory = Process; - PromotedOnly = true; - - trigger OnAction() - var - DirectionsAuth: Codeunit "Directions Auth"; - begin - DirectionsAuth.TestConnection(Rec); - Message('Connection test successful!'); - end; - } - } - } - - trigger OnOpenPage() - begin - Rec.GetOrCreate(); - UpdateAPIKeyText(); - end; - - trigger OnAfterGetCurrRecord() - begin - UpdateAPIKeyText(); - end; - - local procedure UpdateAPIKeyText() - begin - if not IsNullGuid(Rec."API Key") then - APIKeyText := Rec.GetAPIKeyText() - else - APIKeyText := ''; - end; - - var - APIKeyText: Text; -} +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Integration; + +/// +/// Setup page for Connector API connection. +/// Allows users to configure the API URL and register to get an API key. +/// +page 50122 "Connector Connection Setup" +{ + Caption = 'Connector Setup'; + PageType = Card; + SourceTable = "Connector Connection Setup"; + InsertAllowed = false; + DeleteAllowed = false; + ApplicationArea = All; + UsageCategory = Administration; + + layout + { + area(content) + { + group(General) + { + Caption = 'Connection Settings'; + + field("API Base URL"; Rec."API Base URL") + { + ApplicationArea = All; + ToolTip = 'Specifies the base URL for the Connector API server (e.g., https://workshop-server.azurewebsites.net/)'; + ShowMandatory = true; + } + field("User Name"; Rec."User Name") + { + ApplicationArea = All; + ToolTip = 'Specifies the user name to register with the API'; + ShowMandatory = true; + } + field(Registered; Rec.Registered) + { + ApplicationArea = All; + ToolTip = 'Indicates whether you have successfully registered with the API'; + Style = Favorable; + StyleExpr = Rec.Registered; + } + field("API Key"; APIKeyText) + { + ApplicationArea = All; + Caption = 'API Key'; + ToolTip = 'Specifies the API key received after registration'; + Editable = false; + ExtendedDatatype = Masked; + + trigger OnAssistEdit() + var + NewAPIKey: Text; + begin + NewAPIKey := APIKeyText; + if NewAPIKey <> '' then begin + Rec.SetAPIKey(NewAPIKey); + Rec.Modify(); + CurrPage.Update(); + end; + end; + } + } + } + } + + actions + { + area(Processing) + { + action(Register) + { + ApplicationArea = All; + Caption = 'Register'; + ToolTip = 'Register with the Connector API to get an API key'; + Image = Approve; + Promoted = true; + PromotedCategory = Process; + PromotedOnly = true; + + trigger OnAction() + var + ConnectorAuth: Codeunit "Connector Auth"; + begin + ConnectorAuth.RegisterUser(Rec); + CurrPage.Update(); + UpdateAPIKeyText(); + Message('Registration successful! Your API key has been saved.'); + end; + } + action(TestConnection) + { + ApplicationArea = All; + Caption = 'Test Connection'; + ToolTip = 'Test the connection to the Connector API'; + Image = ValidateEmailLoggingSetup; + Promoted = true; + PromotedCategory = Process; + PromotedOnly = true; + + trigger OnAction() + var + ConnectorAuth: Codeunit "Connector Auth"; + begin + ConnectorAuth.TestConnection(Rec); + Message('Connection test successful!'); + end; + } + } + } + + trigger OnOpenPage() + begin + Rec.GetOrCreate(); + UpdateAPIKeyText(); + end; + + trigger OnAfterGetCurrRecord() + begin + UpdateAPIKeyText(); + end; + + local procedure UpdateAPIKeyText() + begin + if not IsNullGuid(Rec."API Key") then + APIKeyText := Rec.GetAPIKeyText() + else + APIKeyText := ''; + end; + + var + APIKeyText: Text; +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Table.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Table.al similarity index 91% rename from samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Table.al rename to samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Table.al index a4df6d18..6f7c7d10 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsConnectionSetup.Table.al +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Table.al @@ -1,94 +1,94 @@ -// ------------------------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// ------------------------------------------------------------------------------------------------ -namespace Microsoft.EServices.EDocument.Integration; - -/// -/// Stores connection settings for DirectionsConnector API. -/// This is a singleton table that holds the API URL and authentication key. -/// -table 81100 "Directions Connection Setup" -{ - Caption = 'Directions Connection Setup'; - DataClassification = CustomerContent; - - fields - { - field(1; "Primary Key"; Code[10]) - { - Caption = 'Primary Key'; - DataClassification = SystemMetadata; - } - field(10; "API Base URL"; Text[250]) - { - Caption = 'API Base URL'; - DataClassification = CustomerContent; - - trigger OnValidate() - begin - if "API Base URL" <> '' then - if not "API Base URL".EndsWith('/') then - "API Base URL" := "API Base URL" + '/'; - end; - } - field(11; "API Key"; Guid) - { - Caption = 'API Key'; - DataClassification = EndUserIdentifiableInformation; - } - field(20; "User Name"; Text[100]) - { - Caption = 'User Name'; - DataClassification = EndUserIdentifiableInformation; - } - field(21; Registered; Boolean) - { - Caption = 'Registered'; - DataClassification = CustomerContent; - Editable = false; - } - } - - keys - { - key(PK; "Primary Key") - { - Clustered = true; - } - } - - /// - /// Gets or creates the singleton setup record. - /// - procedure GetOrCreate(): Boolean - begin - if not Get() then begin - Init(); - "Primary Key" := ''; - Insert(); - end; - exit(true); - end; - - /// - /// Sets the API key from text value. - /// - procedure SetAPIKey(NewAPIKey: Text) - var - APIKeyGuid: Guid; - begin - if Evaluate(APIKeyGuid, NewAPIKey) then - "API Key" := APIKeyGuid - else - Error('Invalid API Key format. Must be a valid GUID.'); - end; - - /// - /// Gets the API key as text. - /// - procedure GetAPIKeyText(): Text - begin - exit(Format("API Key")); - end; -} +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Integration; + +/// +/// Stores connection settings for Connector API. +/// This is a singleton table that holds the API URL and authentication key. +/// +table 50122 "Connector Connection Setup" +{ + Caption = 'Connector Connection Setup'; + DataClassification = CustomerContent; + + fields + { + field(1; "Primary Key"; Code[10]) + { + Caption = 'Primary Key'; + DataClassification = SystemMetadata; + } + field(10; "API Base URL"; Text[250]) + { + Caption = 'API Base URL'; + DataClassification = CustomerContent; + + trigger OnValidate() + begin + if "API Base URL" <> '' then + if not "API Base URL".EndsWith('/') then + "API Base URL" := "API Base URL" + '/'; + end; + } + field(11; "API Key"; Guid) + { + Caption = 'API Key'; + DataClassification = EndUserIdentifiableInformation; + } + field(20; "User Name"; Text[100]) + { + Caption = 'User Name'; + DataClassification = EndUserIdentifiableInformation; + } + field(21; Registered; Boolean) + { + Caption = 'Registered'; + DataClassification = CustomerContent; + Editable = false; + } + } + + keys + { + key(PK; "Primary Key") + { + Clustered = true; + } + } + + /// + /// Gets or creates the singleton setup record. + /// + procedure GetOrCreate(): Boolean + begin + if not Get() then begin + Init(); + "Primary Key" := ''; + Insert(); + end; + exit(true); + end; + + /// + /// Sets the API key from text value. + /// + procedure SetAPIKey(NewAPIKey: Text) + var + APIKeyGuid: Guid; + begin + if Evaluate(APIKeyGuid, NewAPIKey) then + "API Key" := APIKeyGuid + else + Error('Invalid API Key format. Must be a valid GUID.'); + end; + + /// + /// Gets the API key as text. + /// + procedure GetAPIKeyText(): Text + begin + exit(Format("API Key")); + end; +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.Codeunit.al new file mode 100644 index 00000000..13737c2b --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.Codeunit.al @@ -0,0 +1,195 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Integration; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Integration.Send; +using Microsoft.eServices.EDocument.Integration.Receive; +using Microsoft.eServices.EDocument.Integration.Interfaces; +using System.Utilities; + +/// +/// Implements the IDocumentSender and IDocumentReceiver interfaces for Connector. +/// This codeunit handles sending and receiving E-Documents via the Connector API. +/// +codeunit 50123 "Connector Integration" implements IDocumentSender, IDocumentReceiver +{ + Access = Internal; + + // ============================================================================ + // SENDING DOCUMENTS + // ============================================================================ + + // ============================================================================ + // TODO: Exercise 2.A (10 minutes) + // Send an E-Document to the Connector API. + // + // TASK: Do the TODOs in the Send procedure below + // ============================================================================ + procedure Send(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; SendContext: Codeunit SendContext) + var + ConnectorSetup: Record "Connector Connection Setup"; + ConnectorAuth: Codeunit "Connector Auth"; + ConnectorRequests: Codeunit "Connector Requests"; + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + TempBlob: Codeunit "Temp Blob"; + JsonContent, APIEndpoint : Text; + begin + ConnectorAuth.GetConnectionSetup(ConnectorSetup); + + // TODO: Get temp blob with json from SendContext + // - Tips: Use SendContext.GetTempBlob() + // - Tips: Use ConnectorRequests.ReadJsonFromBlob(TempBlob) to read json text from blob + + // + + // TODO: Create POST request to 'enqueue' endpoint + // - Tips: Add enqueue to the base URL from ConnectorSetup + + // + + ConnectorRequests.CreatePostRequest(APIEndpoint, JsonContent, HttpRequest); + ConnectorAuth.AddAuthHeader(HttpRequest, ConnectorSetup); + + // TODO: Send the HTTP request and handle the response using HttpClient + + // + + SendContext.Http().SetHttpRequestMessage(HttpRequest); + SendContext.Http().SetHttpResponseMessage(HttpResponse); + ConnectorRequests.CheckResponseSuccess(HttpResponse); + end; + + // ============================================================================ + // RECEIVING DOCUMENTS + // ============================================================================ + + // ============================================================================ + // TODO: Exercise 2.B (10 minutes) + // Receive a list of documents from the Connector API. + // + // TASK: Do the todos + // ============================================================================ + procedure ReceiveDocuments(var EDocumentService: Record "E-Document Service"; DocumentsMetadata: Codeunit "Temp Blob List"; ReceiveContext: Codeunit ReceiveContext) + var + ConnectorSetup: Record "Connector Connection Setup"; + ConnectorAuth: Codeunit "Connector Auth"; + ConnectorRequests: Codeunit "Connector Requests"; + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + JsonObject: JsonObject; + JsonToken: JsonToken; + JsonArray: JsonArray; + TempBlob: Codeunit "Temp Blob"; + ResponseText: Text; + DocumentJson: Text; + APIEndpoint: Text; + begin + ConnectorAuth.GetConnectionSetup(ConnectorSetup); + + + // TODO: Create Get request to 'peek' endpoint + // - Tips: Add peek to the base URL from ConnectorSetup + + // + + ConnectorRequests.CreateGetRequest(APIEndpoint, HttpRequest); + ConnectorAuth.AddAuthHeader(HttpRequest, ConnectorSetup); + + + // TODO: Send the HTTP request and handle the response using HttpClient + + // + + + ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); + ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); + ConnectorRequests.CheckResponseSuccess(HttpResponse); + + ResponseText := ConnectorRequests.GetResponseText(HttpResponse); + + if JsonObject.ReadFrom(ResponseText) then begin + if JsonObject.Get('items', JsonToken) then begin + JsonArray := JsonToken.AsArray(); + foreach JsonToken in JsonArray do begin + Clear(TempBlob); + JsonToken.WriteTo(DocumentJson); + ConnectorRequests.WriteTextToBlob(DocumentJson, TempBlob); + + // TODO: Add TempBlob to DocumentsMetadata so we can process it later in DownloadDocument + // + end; + end; + end; + end; + + // ============================================================================ + // TODO: Exercise 2.C (5 minutes) + // Download a single document from the Connector API (dequeue). + // + // TASK: Do the todos + // ============================================================================ + procedure DownloadDocument(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; DocumentMetadata: codeunit "Temp Blob"; ReceiveContext: Codeunit ReceiveContext) + var + ConnectorSetup: Record "Connector Connection Setup"; + ConnectorAuth: Codeunit "Connector Auth"; + ConnectorRequests: Codeunit "Connector Requests"; + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + JsonObject: JsonObject; + JsonToken: JsonToken; + TempBlob: Codeunit "Temp Blob"; + ResponseText: Text; + DocumentJson: Text; + APIEndpoint: Text; + begin + ConnectorAuth.GetConnectionSetup(ConnectorSetup); + + // TODO: Create Get request to 'dequeue' endpoint + // - Tips: Add dequeue to the base URL from ConnectorSetup + + // ` + + ConnectorRequests.CreateGetRequest(APIEndpoint, HttpRequest); + ConnectorAuth.AddAuthHeader(HttpRequest, ConnectorSetup); + + // TODO: Send the HTTP request and handle the response using HttpClient + // + + + ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); + ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); + ConnectorRequests.CheckResponseSuccess(HttpResponse); + + ResponseText := ConnectorRequests.GetResponseText(HttpResponse); + if JsonObject.ReadFrom(ResponseText) then begin + if JsonObject.Get('document', JsonToken) then begin + JsonToken.WriteTo(DocumentJson); + TempBlob := ReceiveContext.GetTempBlob(); + ConnectorRequests.WriteTextToBlob(DocumentJson, TempBlob); + end else + Error('No document found in response.'); + end; + end; + + // ============================================================================ + // Event Subscriber - Opens setup page when configuring the service + // ============================================================================ + [EventSubscriber(ObjectType::Page, Page::"E-Document Service", OnBeforeOpenServiceIntegrationSetupPage, '', false, false)] + local procedure OnBeforeOpenServiceIntegrationSetupPage(EDocumentService: Record "E-Document Service"; var IsServiceIntegrationSetupRun: Boolean) + var + ConnectorSetup: Page "Connector Connection Setup"; + begin + if EDocumentService."Service Integration V2" <> EDocumentService."Service Integration V2"::Connector then + exit; + + ConnectorSetup.RunModal(); + IsServiceIntegrationSetupRun := true; + end; +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.EnumExt.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.EnumExt.al similarity index 58% rename from samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.EnumExt.al rename to samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.EnumExt.al index 716bb546..5d91f226 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.EnumExt.al +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.EnumExt.al @@ -1,22 +1,22 @@ -// ------------------------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// ------------------------------------------------------------------------------------------------ -namespace Microsoft.EServices.EDocument.Integration; - -using Microsoft.eServices.EDocument.Integration.Interfaces; - -/// -/// Extends the Service Integration V2 enum to add DirectionsConnector integration. -/// This enum value is used to identify which integration implementation to use. -/// -enumextension 81100 "Directions Integration" extends "Service Integration V2" -{ - value(81100; "Directions Connector") - { - Caption = 'Directions Connector'; - Implementation = IDocumentSender = "Directions Integration Impl.", - IDocumentReceiver = "Directions Integration Impl."; - } - -} +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Integration; + +using Microsoft.eServices.EDocument.Integration.Interfaces; + +/// +/// Extends the Service Integration V2 enum to add Connector integration. +/// This enum value is used to identify which integration implementation to use. +/// +enumextension 50124 "Connector Integration" extends "Service Integration" +{ + value(50124; "Connector") + { + Caption = 'Connector'; + Implementation = IDocumentSender = "Connector Integration", + IDocumentReceiver = "Connector Integration"; + } + +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsRequests.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorRequests.Codeunit.al similarity index 87% rename from samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsRequests.Codeunit.al rename to samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorRequests.Codeunit.al index 8b2e665d..911d2a56 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsRequests.Codeunit.al +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorRequests.Codeunit.al @@ -1,89 +1,93 @@ -// ------------------------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// ------------------------------------------------------------------------------------------------ -namespace Microsoft.EServices.EDocument.Integration; - -using System.Utilities; - -/// -/// Helper codeunit for building HTTP requests for the Directions API. -/// Pre-written to save time during the workshop. -/// -codeunit 81102 "Directions Requests" -{ - Access = Internal; - - /// - /// Creates an HTTP POST request with JSON content. - /// - procedure CreatePostRequest(Url: Text; JsonContent: Text; var HttpRequest: HttpRequestMessage) - var - HttpContent: HttpContent; - begin - HttpContent.WriteFrom(JsonContent); - HttpContent.GetHeaders().Remove('Content-Type'); - HttpContent.GetHeaders().Add('Content-Type', 'application/json'); - - HttpRequest.Method := 'POST'; - HttpRequest.SetRequestUri(Url); - HttpRequest.Content := HttpContent; - end; - - /// - /// Creates an HTTP GET request. - /// - procedure CreateGetRequest(Url: Text; var HttpRequest: HttpRequestMessage) - begin - HttpRequest.Method := 'GET'; - HttpRequest.SetRequestUri(Url); - end; - - /// - /// Reads the response content as text. - /// - procedure GetResponseText(HttpResponse: HttpResponseMessage): Text - var - ResponseText: Text; - begin - HttpResponse.Content.ReadAs(ResponseText); - exit(ResponseText); - end; - - /// - /// Checks if the response is successful and throws an error if not. - /// - procedure CheckResponseSuccess(HttpResponse: HttpResponseMessage) - var - ResponseText: Text; - begin - if not HttpResponse.IsSuccessStatusCode() then begin - ResponseText := GetResponseText(HttpResponse); - Error('API request failed with status %1: %2', HttpResponse.HttpStatusCode(), ResponseText); - end; - end; - - /// - /// Reads JSON content from a TempBlob. - /// - procedure ReadJsonFromBlob(var TempBlob: Codeunit "Temp Blob"): Text - var - InStr: InStream; - JsonText: Text; - begin - TempBlob.CreateInStream(InStr, TextEncoding::UTF8); - InStr.ReadText(JsonText); - exit(JsonText); - end; - - /// - /// Writes text content to a TempBlob. - /// - procedure WriteTextToBlob(TextContent: Text; var TempBlob: Codeunit "Temp Blob") - var - OutStr: OutStream; - begin - TempBlob.CreateOutStream(OutStr, TextEncoding::UTF8); - OutStr.WriteText(TextContent); - end; -} +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Integration; + +using System.Utilities; + +/// +/// Helper codeunit for building HTTP requests for the Connector API. +/// Pre-written to save time during the workshop. +/// +codeunit 50125 "Connector Requests" +{ + Access = Internal; + + /// + /// Creates an HTTP POST request with JSON content. + /// + procedure CreatePostRequest(Url: Text; JsonContent: Text; var HttpRequest: HttpRequestMessage) + var + HttpHeaders: HttpHeaders; + HttpContent: HttpContent; + begin + HttpContent.WriteFrom(JsonContent); + // Prepare HTTP request + HttpRequest.GetHeaders(HttpHeaders); + if HttpHeaders.Contains('Content-Type') then + HttpHeaders.Remove('Content-Type'); + HttpHeaders.Add('Content-Type', 'application/json'); + + HttpRequest.Method := 'POST'; + HttpRequest.SetRequestUri(Url); + HttpRequest.Content := HttpContent; + end; + + /// + /// Creates an HTTP GET request. + /// + procedure CreateGetRequest(Url: Text; var HttpRequest: HttpRequestMessage) + begin + HttpRequest.Method := 'GET'; + HttpRequest.SetRequestUri(Url); + end; + + /// + /// Reads the response content as text. + /// + procedure GetResponseText(HttpResponse: HttpResponseMessage): Text + var + ResponseText: Text; + begin + HttpResponse.Content.ReadAs(ResponseText); + exit(ResponseText); + end; + + /// + /// Checks if the response is successful and throws an error if not. + /// + procedure CheckResponseSuccess(HttpResponse: HttpResponseMessage) + var + ResponseText: Text; + begin + if not HttpResponse.IsSuccessStatusCode() then begin + ResponseText := GetResponseText(HttpResponse); + Error('API request failed with status %1: %2', HttpResponse.HttpStatusCode(), ResponseText); + end; + end; + + /// + /// Reads JSON content from a TempBlob. + /// + procedure ReadJsonFromBlob(var TempBlob: Codeunit "Temp Blob"): Text + var + InStr: InStream; + JsonText: Text; + begin + TempBlob.CreateInStream(InStr, TextEncoding::UTF8); + InStr.ReadText(JsonText); + exit(JsonText); + end; + + /// + /// Writes text content to a TempBlob. + /// + procedure WriteTextToBlob(TextContent: Text; var TempBlob: Codeunit "Temp Blob") + var + OutStr: OutStream; + begin + TempBlob.CreateOutStream(OutStr, TextEncoding::UTF8); + OutStr.WriteText(TextContent); + end; +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorTests.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorTests.Codeunit.al new file mode 100644 index 00000000..21d6c39b --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorTests.Codeunit.al @@ -0,0 +1,239 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.EServices.EDocument.Format; + +using System.TestLibraries.Utilities; +using Microsoft.eServices.EDocument; +using Microsoft.EServices.EDocument.Integration; +using Microsoft.eServices.EDocument.Integration.Send; +using Microsoft.eServices.EDocument.Integration.Receive; +using System.Utilities; + +/// +/// Test suite for Connector Integration exercises. +/// Tests verify that the three main exercises are implemented correctly: +/// - Exercise 2.A: Send (enqueue documents) +/// - Exercise 2.B: ReceiveDocuments (peek at documents) +/// - Exercise 2.C: DownloadDocument (dequeue documents) +/// +codeunit 50129 "Connector Tests" +{ + Subtype = Test; + + var + Assert: Codeunit "Library Assert"; + LibraryLowerPermission: Codeunit "Library - Lower Permissions"; + TestCustomerNo: Code[20]; + TestVendorNo: Code[20]; + + local procedure Initialize() + begin + LibraryLowerPermission.SetOutsideO365Scope(); + TestCustomerNo := 'CUST1'; + TestVendorNo := 'VEND1'; + end; + + // ============================================================================ + // EXERCISE 2.A: Test Send (Enqueue) functionality + // Verifies that the Send procedure correctly: + // - Gets the TempBlob from SendContext + // - Reads JSON content from the blob + // - Creates POST request to /enqueue endpoint + // - Sends the request using HttpClient + // ============================================================================ + [Test] + procedure TestExercise2A_Send() + var + EDocument: Record "E-Document"; + EDocumentService: Record "E-Document Service"; + ConnectorIntegration: Codeunit "Connector Integration"; + SendContext: Codeunit SendContext; + TempBlob: Codeunit "Temp Blob"; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + HttpHeaders: HttpHeaders; + JsonText: Text; + RequestUri: Text; + begin + Initialize(); + SetupConnectionSetup(); + + // [GIVEN] A JSON document to send + JsonText := GetTestSalesInvoiceJson(); + WriteJsonToBlob(JsonText, TempBlob); + SendContext.SetTempBlob(TempBlob); + + // [WHEN] Calling Send to enqueue the document + asserterror ConnectorIntegration.Send(EDocument, EDocumentService, SendContext); + + // [THEN] The HTTP request should be created correctly + HttpRequest := SendContext.Http().GetHttpRequestMessage(); + HttpResponse := SendContext.Http().GetHttpResponseMessage(); + + // Verify POST method was used + Assert.AreEqual('POST', HttpRequest.Method(), 'Should use POST method'); + + // Verify the endpoint is /enqueue + RequestUri := HttpRequest.GetRequestUri(); + Assert.IsTrue(RequestUri.EndsWith('/enqueue'), 'Should call /enqueue endpoint'); + + // Verify the request has content + HttpRequest.Content.GetHeaders(HttpHeaders); + Assert.IsTrue(HttpHeaders.Contains('Content-Type'), 'Should have Content-Type header'); + end; + + // ============================================================================ + // EXERCISE 2.B: Test ReceiveDocuments (Peek) functionality + // Verifies that the ReceiveDocuments procedure correctly: + // - Creates GET request to /peek endpoint + // - Sends the request using HttpClient + // - Parses the JSON response array + // - Adds each document to DocumentsMetadata + // ============================================================================ + [Test] + procedure TestExercise2B_ReceiveDocuments() + var + EDocumentService: Record "E-Document Service"; + ConnectorIntegration: Codeunit "Connector Integration"; + ReceiveContext: Codeunit ReceiveContext; + DocumentsMetadata: Codeunit "Temp Blob List"; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + RequestUri: Text; + begin + Initialize(); + SetupConnectionSetup(); + + // [WHEN] Calling ReceiveDocuments to peek at available documents + asserterror ConnectorIntegration.ReceiveDocuments(EDocumentService, DocumentsMetadata, ReceiveContext); + + // [THEN] The HTTP request should be created correctly + HttpRequest := ReceiveContext.Http().GetHttpRequestMessage(); + HttpResponse := ReceiveContext.Http().GetHttpResponseMessage(); + + // Verify GET method was used + Assert.AreEqual('GET', HttpRequest.Method(), 'Should use GET method'); + + // Verify the endpoint is /peek + RequestUri := HttpRequest.GetRequestUri(); + Assert.IsTrue(RequestUri.EndsWith('/peek'), 'Should call /peek endpoint'); + end; + + // ============================================================================ + // EXERCISE 2.C: Test DownloadDocument (Dequeue) functionality + // Verifies that the DownloadDocument procedure correctly: + // - Creates GET request to /dequeue endpoint + // - Sends the request using HttpClient + // - Parses the document from JSON response + // - Writes the document to TempBlob in ReceiveContext + // ============================================================================ + [Test] + procedure TestExercise2C_DownloadDocument() + var + EDocument: Record "E-Document"; + EDocumentService: Record "E-Document Service"; + ConnectorIntegration: Codeunit "Connector Integration"; + ReceiveContext: Codeunit ReceiveContext; + DocumentMetadata: Codeunit "Temp Blob"; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + RequestUri: Text; + begin + Initialize(); + SetupConnectionSetup(); + + // [WHEN] Calling DownloadDocument to dequeue a document + asserterror ConnectorIntegration.DownloadDocument(EDocument, EDocumentService, DocumentMetadata, ReceiveContext); + + // [THEN] The HTTP request should be created correctly + HttpRequest := ReceiveContext.Http().GetHttpRequestMessage(); + HttpResponse := ReceiveContext.Http().GetHttpResponseMessage(); + + // Verify GET method was used + Assert.AreEqual('GET', HttpRequest.Method(), 'Should use GET method'); + + // Verify the endpoint is /dequeue + RequestUri := HttpRequest.GetRequestUri(); + Assert.IsTrue(RequestUri.EndsWith('/dequeue'), 'Should call /dequeue endpoint'); + end; + + // ============================================================================ + // HELPER: Verify JSON reading and writing helpers work + // ============================================================================ + [Test] + procedure TestJsonHelpers() + var + ConnectorRequests: Codeunit "Connector Requests"; + TempBlob: Codeunit "Temp Blob"; + JsonText: Text; + ReadText: Text; + begin + Initialize(); + // [GIVEN] Test JSON content + JsonText := GetTestSalesInvoiceJson(); + + // [WHEN] Writing JSON to blob and reading it back + ConnectorRequests.WriteTextToBlob(JsonText, TempBlob); + ReadText := ConnectorRequests.ReadJsonFromBlob(TempBlob); + + // [THEN] Content should match + Assert.AreEqual(JsonText, ReadText, 'JSON content should be preserved'); + end; + + // ============================================================================ + // HELPER METHODS + // ============================================================================ + + local procedure SetupConnectionSetup() + var + ConnectorSetup: Record "Connector Connection Setup"; + begin + if not ConnectorSetup.Get() then begin + ConnectorSetup.Init(); + ConnectorSetup."API Base URL" := 'https://test.azurewebsites.net/'; + ConnectorSetup.SetAPIKey('12345678-1234-1234-1234-123456789abc'); + ConnectorSetup.Insert(); + end; + end; + + local procedure GetTestSalesInvoiceJson(): Text + var + JsonObject: JsonObject; + LinesArray: JsonArray; + LineObject: JsonObject; + JsonText: Text; + begin + // Create test JSON that matches the expected format for sales + JsonObject.Add('documentType', 'Invoice'); + JsonObject.Add('documentNo', 'TEST-SI-001'); + JsonObject.Add('customerNo', TestCustomerNo); + JsonObject.Add('customerName', 'Test Customer'); + JsonObject.Add('postingDate', Format(Today(), 0, '--')); + JsonObject.Add('currencyCode', 'USD'); + JsonObject.Add('totalAmount', 1000); + + // Add line + LineObject.Add('lineNo', 10000); + LineObject.Add('type', 'Item'); + LineObject.Add('no', 'ITEM-001'); + LineObject.Add('description', 'Test Item'); + LineObject.Add('quantity', 5); + LineObject.Add('unitPrice', 200); + LineObject.Add('lineAmount', 1000); + LinesArray.Add(LineObject); + JsonObject.Add('lines', LinesArray); + + JsonObject.WriteTo(JsonText); + exit(JsonText); + end; + + local procedure WriteJsonToBlob(JsonText: Text; var TempBlob: Codeunit "Temp Blob") + var + OutStr: OutStream; + begin + TempBlob.CreateOutStream(OutStr, TextEncoding::UTF8); + OutStr.WriteText(JsonText); + end; +} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.Codeunit.al deleted file mode 100644 index 23207355..00000000 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/DirectionsIntegration.Codeunit.al +++ /dev/null @@ -1,157 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// ------------------------------------------------------------------------------------------------ -namespace Microsoft.EServices.EDocument.Integration; - -using Microsoft.eServices.EDocument; -using Microsoft.eServices.EDocument.Integration.Send; -using Microsoft.eServices.EDocument.Integration.Receive; -using Microsoft.eServices.EDocument.Integration.Interfaces; -using System.Utilities; - -/// -/// Implements the IDocumentSender and IDocumentReceiver interfaces for DirectionsConnector. -/// This codeunit handles sending and receiving E-Documents via the Directions API. -/// -codeunit 81100 "Directions Integration Impl." implements IDocumentSender, IDocumentReceiver -{ - Access = Internal; - - // ============================================================================ - // SENDING DOCUMENTS - // ============================================================================ - - // ============================================================================ - // TODO: Exercise 2.B (10 minutes) - // Send an E-Document to the Directions API. - // - // TASK: Uncomment the code below - it's already complete! - // ============================================================================ - procedure Send(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; SendContext: Codeunit SendContext) - var - DirectionsSetup: Record "Directions Connection Setup"; - DirectionsAuth: Codeunit "Directions Auth"; - DirectionsRequests: Codeunit "Directions Requests"; - HttpClient: HttpClient; - HttpRequest: HttpRequestMessage; - HttpResponse: HttpResponseMessage; - TempBlob: Codeunit "Temp Blob"; - JsonContent: Text; - begin - // TODO: Uncomment all the code below - // DirectionsAuth.GetConnectionSetup(DirectionsSetup); - // SendContext.GetTempBlob(TempBlob); - // JsonContent := DirectionsRequests.ReadJsonFromBlob(TempBlob); - // DirectionsRequests.CreatePostRequest(DirectionsSetup."API Base URL" + 'enqueue', JsonContent, HttpRequest); - // DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); - // SendContext.Http().SetHttpRequestMessage(HttpRequest); - // if not HttpClient.Send(HttpRequest, HttpResponse) then - // Error('Failed to send document to API.'); - // SendContext.Http().SetHttpResponseMessage(HttpResponse); - // DirectionsRequests.CheckResponseSuccess(HttpResponse); - end; - - // ============================================================================ - // RECEIVING DOCUMENTS - // ============================================================================ - - // ============================================================================ - // TODO: Exercise 2.C (10 minutes) - // Receive a list of documents from the Directions API. - // - // TASK: Uncomment the code below and fill in the ??? with the correct endpoint - // ============================================================================ - procedure ReceiveDocuments(var EDocumentService: Record "E-Document Service"; DocumentsMetadata: Codeunit "Temp Blob List"; ReceiveContext: Codeunit ReceiveContext) - var - DirectionsSetup: Record "Directions Connection Setup"; - DirectionsAuth: Codeunit "Directions Auth"; - DirectionsRequests: Codeunit "Directions Requests"; - HttpClient: HttpClient; - HttpRequest: HttpRequestMessage; - HttpResponse: HttpResponseMessage; - JsonObject: JsonObject; - JsonToken: JsonToken; - JsonArray: JsonArray; - TempBlob: Codeunit "Temp Blob"; - ResponseText: Text; - DocumentJson: Text; - begin - // TODO: Uncomment the code below and replace ??? with 'peek' - // DirectionsAuth.GetConnectionSetup(DirectionsSetup); - // DirectionsRequests.CreateGetRequest(DirectionsSetup."API Base URL" + '???', HttpRequest); // TODO: What endpoint shows the queue? - // DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); - // ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); - // if not HttpClient.Send(HttpRequest, HttpResponse) then - // Error('Failed to retrieve documents from API.'); - // ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); - // DirectionsRequests.CheckResponseSuccess(HttpResponse); - // ResponseText := DirectionsRequests.GetResponseText(HttpResponse); - // if JsonObject.ReadFrom(ResponseText) then begin - // if JsonObject.Get('items', JsonToken) then begin - // JsonArray := JsonToken.AsArray(); - // foreach JsonToken in JsonArray do begin - // Clear(TempBlob); - // JsonToken.WriteTo(DocumentJson); - // DirectionsRequests.WriteTextToBlob(DocumentJson, TempBlob); - // DocumentsMetadata.Add(TempBlob); - // end; - // end; - // end; - end; - - // ============================================================================ - // TODO: Exercise 2.D (5 minutes) - // Download a single document from the Directions API (dequeue). - // - // TASK: Uncomment the code below and fill in ??? with the correct endpoint - // ============================================================================ - procedure DownloadDocument(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; DocumentMetadata: codeunit "Temp Blob"; ReceiveContext: Codeunit ReceiveContext) - var - DirectionsSetup: Record "Directions Connection Setup"; - DirectionsAuth: Codeunit "Directions Auth"; - DirectionsRequests: Codeunit "Directions Requests"; - HttpClient: HttpClient; - HttpRequest: HttpRequestMessage; - HttpResponse: HttpResponseMessage; - JsonObject: JsonObject; - JsonToken: JsonToken; - TempBlob: Codeunit "Temp Blob"; - ResponseText: Text; - DocumentJson: Text; - begin - // TODO: Uncomment the code below and replace ??? with 'dequeue' - // DirectionsAuth.GetConnectionSetup(DirectionsSetup); - // DirectionsRequests.CreateGetRequest(DirectionsSetup."API Base URL" + '???', HttpRequest); // TODO: What endpoint removes from queue? - // DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); - // ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); - // if not HttpClient.Send(HttpRequest, HttpResponse) then - // Error('Failed to download document from API.'); - // ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); - // DirectionsRequests.CheckResponseSuccess(HttpResponse); - // ResponseText := DirectionsRequests.GetResponseText(HttpResponse); - // if JsonObject.ReadFrom(ResponseText) then begin - // if JsonObject.Get('document', JsonToken) then begin - // JsonToken.WriteTo(DocumentJson); - // ReceiveContext.GetTempBlob(TempBlob); - // DirectionsRequests.WriteTextToBlob(DocumentJson, TempBlob); - // end else - // Error('No document found in response.'); - // end; - end; - - // ============================================================================ - // Event Subscriber - Opens setup page when configuring the service - // ============================================================================ - [EventSubscriber(ObjectType::Page, Page::"E-Document Service", OnBeforeOpenServiceIntegrationSetupPage, '', false, false)] - local procedure OnBeforeOpenServiceIntegrationSetupPage(EDocumentService: Record "E-Document Service"; var IsServiceIntegrationSetupRun: Boolean) - var - DirectionsSetup: Page "Directions Connection Setup"; - begin - if EDocumentService."Service Integration V2" <> EDocumentService."Service Integration V2"::"Directions Connector" then - exit; - - DirectionsSetup.RunModal(); - IsServiceIntegrationSetupRun := true; - end; -} diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/app.json b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/app.json index 57b10d6e..8974ea04 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/app.json +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/app.json @@ -3,7 +3,8 @@ "name": "Directions Connector", "publisher": "Directions EMEA Workshop", "version": "1.0.0.0", - "brief": "DirectionsConnector integration for E-Document workshop", + "application": "27.1.0.0", + "brief": "Connector integration for E-Document workshop", "description": "Workshop extension that implements the E-Document integration interfaces to send/receive documents via HTTP API", "privacyStatement": "https://privacy.microsoft.com", "EULA": "https://www.microsoft.com/en-us/legal/intellectualproperty/copyright", @@ -15,7 +16,19 @@ "id": "e1d97edc-c239-46b4-8d84-6368bdf67c8b", "name": "E-Document Core", "publisher": "Microsoft", - "version": "27.0.0.0" + "version": "27.1.0.0" + }, + { + "id": "dd0be2ea-f733-4d65-bb34-a28f4624fb14", + "name": "Library Assert", + "publisher": "Microsoft", + "version": "27.1.0.0" + }, + { + "id": "5d86850b-0d76-4eca-bd7b-951ad998e997", + "name": "Tests-TestLibraries", + "publisher": "Microsoft", + "version": "27.1.0.0" } ], "screenshots": [], @@ -30,5 +43,6 @@ "NoImplicitWith", "TranslationFile" ], - "target": "Cloud" + "target": "Cloud", + "runtime": "16.0" } diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al index 20913d06..b6df7d73 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al @@ -29,10 +29,11 @@ codeunit 50102 "SimpleJson Format" implements "E-Document" case SourceDocumentHeader.Number of Database::"Sales Invoice Header": begin - // Exercise 1.A Solution - Validate required fields + // Validation complete SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Sell-to Customer No.")).TestField(); - // Note: In a real implementation, also validate Posting Date exists - // For workshop: validation is considered complete + + // TODO: Exercise 1.A: Validate Posting Date + end; end; end; diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonTest.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonTest.Codeunit.al index 30541777..35ee3a45 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonTest.Codeunit.al +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonTest.Codeunit.al @@ -14,7 +14,7 @@ using Microsoft.Purchases.Document; /// Simple test runner for workshop participants. /// Run this codeunit to validate your exercise implementations. /// -codeunit 50113 "SimpleJson Test Runner" +codeunit 50113 "SimpleJson Test" { Subtype = Test; diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/app.json b/samples/EDocument/DirectionsEMEA2025/application/simple_json/app.json index cc0e72cd..073d53c3 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/simple_json/app.json +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/app.json @@ -3,7 +3,7 @@ "name": "SimpleJson E-Document Format", "publisher": "Directions EMEA Workshop", "version": "1.0.0.0", - "application": "28.0.0.0", + "application": "27.1.0.0", "brief": "SimpleJson format for E-Document workshop", "description": "Workshop extension that implements the E-Document format interface for JSON documents", "privacyStatement": "https://privacy.microsoft.com", @@ -16,23 +16,23 @@ "id": "e1d97edc-c239-46b4-8d84-6368bdf67c8b", "name": "E-Document Core", "publisher": "Microsoft", - "version": "28.0.0.0" + "version": "27.1.0.0" }, { "id": "dd0be2ea-f733-4d65-bb34-a28f4624fb14", "name": "Library Assert", "publisher": "Microsoft", - "version": "28.0.0.0" + "version": "27.1.0.0" }, { "id": "5d86850b-0d76-4eca-bd7b-951ad998e997", "name": "Tests-TestLibraries", "publisher": "Microsoft", - "version": "28.0.0.0" + "version": "27.1.0.0" } ], "screenshots": [], - "platform": "28.0.0.0", + "platform": "27.0.0.0", "idRanges": [ { "from": 50100, @@ -43,5 +43,6 @@ "TranslationFile", "NoImplicitWith" ], - "target": "Cloud" + "target": "Cloud", + "runtime": "16.0" } diff --git a/samples/EDocument/DirectionsEMEA2025/server/server.py b/samples/EDocument/DirectionsEMEA2025/server/app.py similarity index 96% rename from samples/EDocument/DirectionsEMEA2025/server/server.py rename to samples/EDocument/DirectionsEMEA2025/server/app.py index 577665d0..ab6bda14 100644 --- a/samples/EDocument/DirectionsEMEA2025/server/server.py +++ b/samples/EDocument/DirectionsEMEA2025/server/app.py @@ -1,51 +1,55 @@ -from fastapi import FastAPI, Request, HTTPException -from collections import defaultdict, deque -import uuid - -app = FastAPI() - -# Simple in-memory stores -auth_keys = {} # user_id -> key -queues = defaultdict(deque) # key -> deque - -@app.post("/register") -async def register(request: Request): - data = await request.json() - name = data.get("name") - if not name: - raise HTTPException(400, "Missing name") - key = str(uuid.uuid4()) - auth_keys[name] = key - queues[key] # Initialize queue - return {"status": "ok", "key": key} - -def get_key(request: Request) -> str: - key = request.headers.get("X-Service-Key") - if not key or key not in queues: - raise HTTPException(401, "Unauthorized or invalid key") - return key - -@app.post("/enqueue") -async def enqueue(request: Request): - key = get_key(request) - doc = await request.json() - queues[key].append(doc) - return {"status": "ok", "queued_count": len(queues[key])} - -@app.get("/dequeue") -async def dequeue(request: Request): - key = get_key(request) - if not queues[key]: - raise HTTPException(404, "Queue empty") - return {"document": queues[key].popleft()} - -@app.get("/peek") -async def peek(request: Request): - key = get_key(request) - return {"queued_count": len(queues[key]), "items": list(queues[key])} - -@app.delete("/clear") -async def clear(request: Request): - key = get_key(request) - queues[key].clear() - return {"status": "cleared"} +from fastapi import FastAPI, Request, HTTPException +from collections import defaultdict, deque +import uuid + +app = FastAPI() + +# Simple in-memory stores +auth_keys = {} # user_id -> key +queues = defaultdict(deque) # key -> deque + +@app.post("/register") +async def register(request: Request): + data = await request.json() + name = data.get("name") + if not name: + raise HTTPException(400, "Missing name") + key = str(uuid.uuid4()) + auth_keys[name] = key + queues[key] # Initialize queue + return {"status": "ok", "key": key} + +def get_key(request: Request) -> str: + key = request.headers.get("X-Service-Key") + if not key or key not in queues: + raise HTTPException(401, "Unauthorized or invalid key") + return key + +@app.post("/enqueue") +async def enqueue(request: Request): + key = get_key(request) + doc = await request.json() + queues[key].append(doc) + return {"status": "ok", "queued_count": len(queues[key])} + +@app.get("/dequeue") +async def dequeue(request: Request): + key = get_key(request) + if not queues[key]: + raise HTTPException(404, "Queue empty") + return {"document": queues[key].popleft()} + +@app.get("/peek") +async def peek(request: Request): + key = get_key(request) + return {"queued_count": len(queues[key]), "items": list(queues[key])} + +@app.delete("/clear") +async def clear(request: Request): + key = get_key(request) + queues[key].clear() + return {"status": "cleared"} + +@app.get("/") +async def root(): + return "Hello world" diff --git a/samples/EDocument/DirectionsEMEA2025/server/requirements.txt b/samples/EDocument/DirectionsEMEA2025/server/requirements.txt index 170703df..8e0578a0 100644 --- a/samples/EDocument/DirectionsEMEA2025/server/requirements.txt +++ b/samples/EDocument/DirectionsEMEA2025/server/requirements.txt @@ -1 +1,2 @@ -fastapi \ No newline at end of file +fastapi +uvicorn[standard] \ No newline at end of file diff --git a/samples/EDocument/DirectionsEMEA2025/API_REFERENCE.md b/samples/EDocument/DirectionsEMEA2025/workshop/API_REFERENCE.md similarity index 95% rename from samples/EDocument/DirectionsEMEA2025/API_REFERENCE.md rename to samples/EDocument/DirectionsEMEA2025/workshop/API_REFERENCE.md index a154e1a0..8f295658 100644 --- a/samples/EDocument/DirectionsEMEA2025/API_REFERENCE.md +++ b/samples/EDocument/DirectionsEMEA2025/workshop/API_REFERENCE.md @@ -1,557 +1,557 @@ -# Directions Connector API Reference - -Complete API documentation for the workshop server hosted on Azure. - ---- - -## Base URL - -``` -https://[workshop-server].azurewebsites.net/ -``` - -**Note**: The actual URL will be provided by the instructor at the start of the workshop. - ---- - -## Authentication - -All endpoints (except `/register`) require authentication via the `X-Service-Key` header. - -```http -X-Service-Key: your-api-key-here -``` - -The API key is obtained by calling the `/register` endpoint. - ---- - -## Endpoints - -### 1. Register User - -Register a new user and receive an API key. - -**Endpoint**: `POST /register` - -**Authentication**: None - -**Request**: -```http -POST /register HTTP/1.1 -Content-Type: application/json - -{ - "name": "participant-name" -} -``` - -**Response** (200 OK): -```json -{ - "status": "ok", - "key": "12345678-1234-1234-1234-123456789abc" -} -``` - -**Example (cURL)**: -```bash -curl -X POST https://workshop-server.azurewebsites.net/register \ - -H "Content-Type: application/json" \ - -d '{"name": "john-doe"}' -``` - -**Example (PowerShell)**: -```powershell -$body = @{ name = "john-doe" } | ConvertTo-Json -Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/register" ` - -Method Post ` - -Body $body ` - -ContentType "application/json" -``` - -**Notes**: -- Each name should be unique to avoid conflicts -- The API key is returned only once - save it securely -- If you lose your key, you need to register again with a different name - ---- - -### 2. Send Document (Enqueue) - -Add a document to your queue. - -**Endpoint**: `POST /enqueue` - -**Authentication**: Required (`X-Service-Key` header) - -**Request**: -```http -POST /enqueue HTTP/1.1 -X-Service-Key: your-api-key -Content-Type: application/json - -{ - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001", - "customerName": "Contoso Ltd.", - "postingDate": "2025-10-21", - "currencyCode": "USD", - "totalAmount": 1250.00, - "lines": [ - { - "lineNo": 1, - "type": "Item", - "no": "ITEM-001", - "description": "Item A", - "quantity": 5, - "unitPrice": 250.00, - "lineAmount": 1250.00 - } - ] -} -``` - -**Response** (200 OK): -```json -{ - "status": "ok", - "queued_count": 3 -} -``` - -**Example (cURL)**: -```bash -curl -X POST https://workshop-server.azurewebsites.net/enqueue \ - -H "X-Service-Key: your-api-key" \ - -H "Content-Type: application/json" \ - -d '{ - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001", - "customerName": "Contoso Ltd.", - "postingDate": "2025-10-21", - "currencyCode": "USD", - "totalAmount": 1250.00, - "lines": [ - { - "lineNo": 1, - "type": "Item", - "no": "ITEM-001", - "description": "Item A", - "quantity": 5, - "unitPrice": 250.00, - "lineAmount": 1250.00 - } - ] - }' -``` - -**Example (PowerShell)**: -```powershell -$headers = @{ - "X-Service-Key" = "your-api-key" - "Content-Type" = "application/json" -} - -$body = @{ - documentType = "Invoice" - documentNo = "SI-001" - customerNo = "C001" - customerName = "Contoso Ltd." - postingDate = "2025-10-21" - currencyCode = "USD" - totalAmount = 1250.00 - lines = @( - @{ - lineNo = 1 - type = "Item" - no = "ITEM-001" - description = "Item A" - quantity = 5 - unitPrice = 250.00 - lineAmount = 1250.00 - } - ) -} | ConvertTo-Json -Depth 10 - -Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/enqueue" ` - -Method Post ` - -Headers $headers ` - -Body $body -``` - -**Notes**: -- The document is added to your personal queue (isolated by API key) -- You can enqueue any valid JSON document structure -- The `queued_count` returns the total number of documents in your queue - ---- - -### 3. Check Queue (Peek) - -View all documents in your queue without removing them. - -**Endpoint**: `GET /peek` - -**Authentication**: Required (`X-Service-Key` header) - -**Request**: -```http -GET /peek HTTP/1.1 -X-Service-Key: your-api-key -``` - -**Response** (200 OK): -```json -{ - "queued_count": 2, - "items": [ - { - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001", - "customerName": "Contoso Ltd.", - "postingDate": "2025-10-21", - "currencyCode": "USD", - "totalAmount": 1250.00, - "lines": [...] - }, - { - "documentType": "Invoice", - "documentNo": "SI-002", - "customerNo": "C002", - "customerName": "Fabrikam Inc.", - "postingDate": "2025-10-21", - "currencyCode": "EUR", - "totalAmount": 850.00, - "lines": [...] - } - ] -} -``` - -**Example (cURL)**: -```bash -curl -X GET https://workshop-server.azurewebsites.net/peek \ - -H "X-Service-Key: your-api-key" -``` - -**Example (PowerShell)**: -```powershell -$headers = @{ "X-Service-Key" = "your-api-key" } -Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/peek" ` - -Method Get ` - -Headers $headers -``` - -**Example (Browser)**: -You can also use browser extensions like "Modify Header Value" to add the header and view in browser: -``` -https://workshop-server.azurewebsites.net/peek -Header: X-Service-Key: your-api-key -``` - -**Notes**: -- Documents remain in the queue after peeking -- Useful for debugging and verifying document submission -- Returns all documents in your queue (FIFO order) - ---- - -### 4. Retrieve Document (Dequeue) - -Retrieve and remove the first document from your queue. - -**Endpoint**: `GET /dequeue` - -**Authentication**: Required (`X-Service-Key` header) - -**Request**: -```http -GET /dequeue HTTP/1.1 -X-Service-Key: your-api-key -``` - -**Response** (200 OK): -```json -{ - "document": { - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001", - "customerName": "Contoso Ltd.", - "postingDate": "2025-10-21", - "currencyCode": "USD", - "totalAmount": 1250.00, - "lines": [ - { - "lineNo": 1, - "type": "Item", - "no": "ITEM-001", - "description": "Item A", - "quantity": 5, - "unitPrice": 250.00, - "lineAmount": 1250.00 - } - ] - } -} -``` - -**Response** (404 Not Found) - Queue Empty: -```json -{ - "detail": "Queue empty" -} -``` - -**Example (cURL)**: -```bash -curl -X GET https://workshop-server.azurewebsites.net/dequeue \ - -H "X-Service-Key: your-api-key" -``` - -**Example (PowerShell)**: -```powershell -$headers = @{ "X-Service-Key" = "your-api-key" } -Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/dequeue" ` - -Method Get ` - -Headers $headers -``` - -**Notes**: -- Documents are removed from the queue after dequeue (FIFO) -- Returns 404 if the queue is empty -- Once dequeued, the document cannot be retrieved again - ---- - -### 5. Clear Queue - -Clear all documents from your queue. - -**Endpoint**: `DELETE /clear` - -**Authentication**: Required (`X-Service-Key` header) - -**Request**: -```http -DELETE /clear HTTP/1.1 -X-Service-Key: your-api-key -``` - -**Response** (200 OK): -```json -{ - "status": "cleared" -} -``` - -**Example (cURL)**: -```bash -curl -X DELETE https://workshop-server.azurewebsites.net/clear \ - -H "X-Service-Key: your-api-key" -``` - -**Example (PowerShell)**: -```powershell -$headers = @{ "X-Service-Key" = "your-api-key" } -Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/clear" ` - -Method Delete ` - -Headers $headers -``` - -**Notes**: -- Removes all documents from your queue -- Useful for testing and cleanup -- Cannot be undone - ---- - -## Error Responses - -All endpoints may return error responses in the following format: - -### 400 Bad Request -```json -{ - "detail": "Missing name" -} -``` -Returned when required parameters are missing or invalid. - -### 401 Unauthorized -```json -{ - "detail": "Unauthorized or invalid key" -} -``` -Returned when: -- The `X-Service-Key` header is missing -- The API key is invalid -- The API key doesn't match any registered user - -### 404 Not Found -```json -{ - "detail": "Queue empty" -} -``` -Returned when trying to dequeue from an empty queue. - ---- - -## Rate Limits - -Currently, there are **no rate limits** enforced. However, please be considerate of other workshop participants and: -- Don't spam the API with excessive requests -- Use reasonable document sizes (< 1 MB) -- Clean up your queue after testing - ---- - -## Data Persistence - -**Important**: -- All data is stored **in-memory only** -- If the server restarts, all queues and registrations are lost -- Don't rely on this API for production use -- This is a workshop server only - ---- - -## JSON Document Structure - -While you can send any valid JSON, the recommended structure for this workshop is: - -```json -{ - "documentType": "Invoice", // Type of document - "documentNo": "string", // Document number - "customerNo": "string", // Customer/Vendor number - "customerName": "string", // Customer/Vendor name - "postingDate": "YYYY-MM-DD", // ISO date format - "currencyCode": "string", // Currency (USD, EUR, etc.) - "totalAmount": 0.00, // Decimal number - "lines": [ // Array of line items - { - "lineNo": 0, // Integer line number - "type": "string", // Item, G/L Account, etc. - "no": "string", // Item/Account number - "description": "string", // Line description - "quantity": 0.00, // Decimal quantity - "unitPrice": 0.00, // Decimal unit price - "lineAmount": 0.00 // Decimal line amount - } - ] -} -``` - ---- - -## Testing Tips - -### Using Browser Developer Tools - -1. Open browser developer tools (F12) -2. Go to Network tab -3. Call the API endpoints -4. Inspect request/response headers and bodies - -### Using Postman - -1. Import the endpoints into Postman -2. Set up environment variables for base URL and API key -3. Create a collection for easy testing - -### Using VS Code REST Client Extension - -Create a `.http` file: - -```http -### Variables -@baseUrl = https://workshop-server.azurewebsites.net -@apiKey = your-api-key-here - -### Register -POST {{baseUrl}}/register -Content-Type: application/json - -{ - "name": "test-user" -} - -### Enqueue Document -POST {{baseUrl}}/enqueue -X-Service-Key: {{apiKey}} -Content-Type: application/json - -{ - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001" -} - -### Peek Queue -GET {{baseUrl}}/peek -X-Service-Key: {{apiKey}} - -### Dequeue Document -GET {{baseUrl}}/dequeue -X-Service-Key: {{apiKey}} - -### Clear Queue -DELETE {{baseUrl}}/clear -X-Service-Key: {{apiKey}} -``` - ---- - -## Workshop Scenarios - -### Scenario 1: Solo Testing -1. Register with your name -2. Send documents from BC -3. Peek to verify they arrived -4. Dequeue them back into BC - -### Scenario 2: Partner Exchange -1. Each partner registers separately -2. Partner A sends documents -3. Partner B receives and processes them -4. Swap roles - -### Scenario 3: Batch Processing -1. Enqueue multiple documents -2. Peek to see the count -3. Dequeue all at once -4. Process in BC - ---- - -## Support - -If you encounter issues with the API: -1. Check your API key is correct -2. Verify the base URL is accessible -3. Check request headers and body format -4. Look at the error response details -5. Ask the instructor for help - ---- - -## Server Implementation - -The server is built with FastAPI (Python) and is extremely simple: -- In-memory storage (dictionary and deques) -- No database -- No authentication beyond the API key -- No encryption (workshop use only) - -See `server/server.py` for the complete source code. - ---- - -**Happy Testing!** πŸš€ +# Directions Connector API Reference + +Complete API documentation for the workshop server hosted on Azure. + +--- + +## Base URL + +``` +https://[workshop-server].azurewebsites.net/ +``` + +**Note**: The actual URL will be provided by the instructor at the start of the workshop. + +--- + +## Authentication + +All endpoints (except `/register`) require authentication via the `X-Service-Key` header. + +```http +X-Service-Key: your-api-key-here +``` + +The API key is obtained by calling the `/register` endpoint. + +--- + +## Endpoints + +### 1. Register User + +Register a new user and receive an API key. + +**Endpoint**: `POST /register` + +**Authentication**: None + +**Request**: +```http +POST /register HTTP/1.1 +Content-Type: application/json + +{ + "name": "participant-name" +} +``` + +**Response** (200 OK): +```json +{ + "status": "ok", + "key": "12345678-1234-1234-1234-123456789abc" +} +``` + +**Example (cURL)**: +```bash +curl -X POST https://workshop-server.azurewebsites.net/register \ + -H "Content-Type: application/json" \ + -d '{"name": "john-doe"}' +``` + +**Example (PowerShell)**: +```powershell +$body = @{ name = "john-doe" } | ConvertTo-Json +Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/register" ` + -Method Post ` + -Body $body ` + -ContentType "application/json" +``` + +**Notes**: +- Each name should be unique to avoid conflicts +- The API key is returned only once - save it securely +- If you lose your key, you need to register again with a different name + +--- + +### 2. Send Document (Enqueue) + +Add a document to your queue. + +**Endpoint**: `POST /enqueue` + +**Authentication**: Required (`X-Service-Key` header) + +**Request**: +```http +POST /enqueue HTTP/1.1 +X-Service-Key: your-api-key +Content-Type: application/json + +{ + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001", + "customerName": "Contoso Ltd.", + "postingDate": "2025-10-21", + "currencyCode": "USD", + "totalAmount": 1250.00, + "lines": [ + { + "lineNo": 1, + "type": "Item", + "no": "ITEM-001", + "description": "Item A", + "quantity": 5, + "unitPrice": 250.00, + "lineAmount": 1250.00 + } + ] +} +``` + +**Response** (200 OK): +```json +{ + "status": "ok", + "queued_count": 3 +} +``` + +**Example (cURL)**: +```bash +curl -X POST https://workshop-server.azurewebsites.net/enqueue \ + -H "X-Service-Key: your-api-key" \ + -H "Content-Type: application/json" \ + -d '{ + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001", + "customerName": "Contoso Ltd.", + "postingDate": "2025-10-21", + "currencyCode": "USD", + "totalAmount": 1250.00, + "lines": [ + { + "lineNo": 1, + "type": "Item", + "no": "ITEM-001", + "description": "Item A", + "quantity": 5, + "unitPrice": 250.00, + "lineAmount": 1250.00 + } + ] + }' +``` + +**Example (PowerShell)**: +```powershell +$headers = @{ + "X-Service-Key" = "your-api-key" + "Content-Type" = "application/json" +} + +$body = @{ + documentType = "Invoice" + documentNo = "SI-001" + customerNo = "C001" + customerName = "Contoso Ltd." + postingDate = "2025-10-21" + currencyCode = "USD" + totalAmount = 1250.00 + lines = @( + @{ + lineNo = 1 + type = "Item" + no = "ITEM-001" + description = "Item A" + quantity = 5 + unitPrice = 250.00 + lineAmount = 1250.00 + } + ) +} | ConvertTo-Json -Depth 10 + +Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/enqueue" ` + -Method Post ` + -Headers $headers ` + -Body $body +``` + +**Notes**: +- The document is added to your personal queue (isolated by API key) +- You can enqueue any valid JSON document structure +- The `queued_count` returns the total number of documents in your queue + +--- + +### 3. Check Queue (Peek) + +View all documents in your queue without removing them. + +**Endpoint**: `GET /peek` + +**Authentication**: Required (`X-Service-Key` header) + +**Request**: +```http +GET /peek HTTP/1.1 +X-Service-Key: your-api-key +``` + +**Response** (200 OK): +```json +{ + "queued_count": 2, + "items": [ + { + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001", + "customerName": "Contoso Ltd.", + "postingDate": "2025-10-21", + "currencyCode": "USD", + "totalAmount": 1250.00, + "lines": [...] + }, + { + "documentType": "Invoice", + "documentNo": "SI-002", + "customerNo": "C002", + "customerName": "Fabrikam Inc.", + "postingDate": "2025-10-21", + "currencyCode": "EUR", + "totalAmount": 850.00, + "lines": [...] + } + ] +} +``` + +**Example (cURL)**: +```bash +curl -X GET https://workshop-server.azurewebsites.net/peek \ + -H "X-Service-Key: your-api-key" +``` + +**Example (PowerShell)**: +```powershell +$headers = @{ "X-Service-Key" = "your-api-key" } +Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/peek" ` + -Method Get ` + -Headers $headers +``` + +**Example (Browser)**: +You can also use browser extensions like "Modify Header Value" to add the header and view in browser: +``` +https://workshop-server.azurewebsites.net/peek +Header: X-Service-Key: your-api-key +``` + +**Notes**: +- Documents remain in the queue after peeking +- Useful for debugging and verifying document submission +- Returns all documents in your queue (FIFO order) + +--- + +### 4. Retrieve Document (Dequeue) + +Retrieve and remove the first document from your queue. + +**Endpoint**: `GET /dequeue` + +**Authentication**: Required (`X-Service-Key` header) + +**Request**: +```http +GET /dequeue HTTP/1.1 +X-Service-Key: your-api-key +``` + +**Response** (200 OK): +```json +{ + "document": { + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001", + "customerName": "Contoso Ltd.", + "postingDate": "2025-10-21", + "currencyCode": "USD", + "totalAmount": 1250.00, + "lines": [ + { + "lineNo": 1, + "type": "Item", + "no": "ITEM-001", + "description": "Item A", + "quantity": 5, + "unitPrice": 250.00, + "lineAmount": 1250.00 + } + ] + } +} +``` + +**Response** (404 Not Found) - Queue Empty: +```json +{ + "detail": "Queue empty" +} +``` + +**Example (cURL)**: +```bash +curl -X GET https://workshop-server.azurewebsites.net/dequeue \ + -H "X-Service-Key: your-api-key" +``` + +**Example (PowerShell)**: +```powershell +$headers = @{ "X-Service-Key" = "your-api-key" } +Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/dequeue" ` + -Method Get ` + -Headers $headers +``` + +**Notes**: +- Documents are removed from the queue after dequeue (FIFO) +- Returns 404 if the queue is empty +- Once dequeued, the document cannot be retrieved again + +--- + +### 5. Clear Queue + +Clear all documents from your queue. + +**Endpoint**: `DELETE /clear` + +**Authentication**: Required (`X-Service-Key` header) + +**Request**: +```http +DELETE /clear HTTP/1.1 +X-Service-Key: your-api-key +``` + +**Response** (200 OK): +```json +{ + "status": "cleared" +} +``` + +**Example (cURL)**: +```bash +curl -X DELETE https://workshop-server.azurewebsites.net/clear \ + -H "X-Service-Key: your-api-key" +``` + +**Example (PowerShell)**: +```powershell +$headers = @{ "X-Service-Key" = "your-api-key" } +Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/clear" ` + -Method Delete ` + -Headers $headers +``` + +**Notes**: +- Removes all documents from your queue +- Useful for testing and cleanup +- Cannot be undone + +--- + +## Error Responses + +All endpoints may return error responses in the following format: + +### 400 Bad Request +```json +{ + "detail": "Missing name" +} +``` +Returned when required parameters are missing or invalid. + +### 401 Unauthorized +```json +{ + "detail": "Unauthorized or invalid key" +} +``` +Returned when: +- The `X-Service-Key` header is missing +- The API key is invalid +- The API key doesn't match any registered user + +### 404 Not Found +```json +{ + "detail": "Queue empty" +} +``` +Returned when trying to dequeue from an empty queue. + +--- + +## Rate Limits + +Currently, there are **no rate limits** enforced. However, please be considerate of other workshop participants and: +- Don't spam the API with excessive requests +- Use reasonable document sizes (< 1 MB) +- Clean up your queue after testing + +--- + +## Data Persistence + +**Important**: +- All data is stored **in-memory only** +- If the server restarts, all queues and registrations are lost +- Don't rely on this API for production use +- This is a workshop server only + +--- + +## JSON Document Structure + +While you can send any valid JSON, the recommended structure for this workshop is: + +```json +{ + "documentType": "Invoice", // Type of document + "documentNo": "string", // Document number + "customerNo": "string", // Customer/Vendor number + "customerName": "string", // Customer/Vendor name + "postingDate": "YYYY-MM-DD", // ISO date format + "currencyCode": "string", // Currency (USD, EUR, etc.) + "totalAmount": 0.00, // Decimal number + "lines": [ // Array of line items + { + "lineNo": 0, // Integer line number + "type": "string", // Item, G/L Account, etc. + "no": "string", // Item/Account number + "description": "string", // Line description + "quantity": 0.00, // Decimal quantity + "unitPrice": 0.00, // Decimal unit price + "lineAmount": 0.00 // Decimal line amount + } + ] +} +``` + +--- + +## Testing Tips + +### Using Browser Developer Tools + +1. Open browser developer tools (F12) +2. Go to Network tab +3. Call the API endpoints +4. Inspect request/response headers and bodies + +### Using Postman + +1. Import the endpoints into Postman +2. Set up environment variables for base URL and API key +3. Create a collection for easy testing + +### Using VS Code REST Client Extension + +Create a `.http` file: + +```http +### Variables +@baseUrl = https://workshop-server.azurewebsites.net +@apiKey = your-api-key-here + +### Register +POST {{baseUrl}}/register +Content-Type: application/json + +{ + "name": "test-user" +} + +### Enqueue Document +POST {{baseUrl}}/enqueue +X-Service-Key: {{apiKey}} +Content-Type: application/json + +{ + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001" +} + +### Peek Queue +GET {{baseUrl}}/peek +X-Service-Key: {{apiKey}} + +### Dequeue Document +GET {{baseUrl}}/dequeue +X-Service-Key: {{apiKey}} + +### Clear Queue +DELETE {{baseUrl}}/clear +X-Service-Key: {{apiKey}} +``` + +--- + +## Workshop Scenarios + +### Scenario 1: Solo Testing +1. Register with your name +2. Send documents from BC +3. Peek to verify they arrived +4. Dequeue them back into BC + +### Scenario 2: Partner Exchange +1. Each partner registers separately +2. Partner A sends documents +3. Partner B receives and processes them +4. Swap roles + +### Scenario 3: Batch Processing +1. Enqueue multiple documents +2. Peek to see the count +3. Dequeue all at once +4. Process in BC + +--- + +## Support + +If you encounter issues with the API: +1. Check your API key is correct +2. Verify the base URL is accessible +3. Check request headers and body format +4. Look at the error response details +5. Ask the instructor for help + +--- + +## Server Implementation + +The server is built with FastAPI (Python) and is extremely simple: +- In-memory storage (dictionary and deques) +- No database +- No authentication beyond the API key +- No encryption (workshop use only) + +See `server/server.py` for the complete source code. + +--- + +**Happy Testing!** πŸš€ diff --git a/samples/EDocument/DirectionsEMEA2025/README.md b/samples/EDocument/DirectionsEMEA2025/workshop/README.md similarity index 97% rename from samples/EDocument/DirectionsEMEA2025/README.md rename to samples/EDocument/DirectionsEMEA2025/workshop/README.md index bbe40796..6f6fe29b 100644 --- a/samples/EDocument/DirectionsEMEA2025/README.md +++ b/samples/EDocument/DirectionsEMEA2025/workshop/README.md @@ -1,277 +1,277 @@ -# E-Document Connector Workshop -## Directions EMEA 2025 - -Welcome to the E-Document Connector Workshop! This hands-on workshop teaches you how to build complete E-Document integrations using the Business Central E-Document Core framework. - ---- - -## 🎯 Workshop Overview - -**Duration**: 90 minutes (10 min intro + 80 min hands-on) - -**What You'll Build**: -1. **SimpleJson Format** - E-Document format implementation for JSON documents -2. **DirectionsConnector** - HTTP API integration for sending/receiving documents -3. **Complete Round-Trip** - Full document exchange between Business Central and external systems - -**What You'll Learn**: -- How the E-Document Core framework works -- How to implement format interfaces -- How to implement integration interfaces -- Best practices for E-Document solutions - ---- - -## πŸ“‚ Workshop Structure - -``` -DirectionsEMEA2025/ -β”œβ”€β”€ WORKSHOP_INTRO.md # πŸ“Š VS Code presentation (10-minute intro) -β”œβ”€β”€ WORKSHOP_GUIDE.md # πŸ“˜ Step-by-step exercises with solutions -β”œβ”€β”€ API_REFERENCE.md # πŸ”Œ Complete API documentation -β”œβ”€β”€ WORKSHOP_PLAN.md # πŸ“ Detailed implementation plan -β”‚ -β”œβ”€β”€ application/ -β”‚ β”œβ”€β”€ simple_json/ # Exercise 1: Format Extension -β”‚ β”‚ β”œβ”€β”€ app.json -β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al -β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al # ⚠️ TODO sections -β”‚ β”‚ └── SimpleJsonHelper.Codeunit.al # βœ… Pre-written -β”‚ β”‚ -β”‚ └── directions_connector/ # Exercise 2: Integration Extension -β”‚ β”œβ”€β”€ app.json -β”‚ β”œβ”€β”€ DirectionsIntegration.EnumExt.al -β”‚ β”œβ”€β”€ DirectionsIntegration.Codeunit.al # ⚠️ TODO sections -β”‚ β”œβ”€β”€ DirectionsConnectionSetup.Table.al # βœ… Pre-written -β”‚ β”œβ”€β”€ DirectionsConnectionSetup.Page.al # βœ… Pre-written -β”‚ β”œβ”€β”€ DirectionsAuth.Codeunit.al # βœ… Pre-written -β”‚ └── DirectionsRequests.Codeunit.al # βœ… Pre-written -β”‚ -β”œβ”€β”€ solution/ # πŸ“¦ Complete working solution (instructor) -β”‚ β”œβ”€β”€ simple_json/ -β”‚ └── directions_connector/ -β”‚ -└── server/ # 🐍 Python API server (Azure hosted) - β”œβ”€β”€ server.py - β”œβ”€β”€ requirements.txt - └── README.md -``` - ---- - -## πŸš€ Getting Started - -### For Participants - -1. **Read the Introduction** - Open `WORKSHOP_INTRO.md` in VS Code (Ctrl+Shift+V for preview) -2. **Follow the Guide** - Use `WORKSHOP_GUIDE.md` for step-by-step instructions -3. **Reference the API** - Check `API_REFERENCE.md` for endpoint details -4. **Ask Questions** - The instructor is here to help! - -### For Instructors - -1. **Review the Plan** - See `WORKSHOP_PLAN.md` for complete overview -2. **Present the Intro** - Use `WORKSHOP_INTRO.md` as your slide deck in VS Code -3. **Provide API URL** - Update the API Base URL in materials before workshop -4. **Reference Solution** - Complete implementation in `/solution/` folder -5. **Monitor Progress** - Check `/peek` endpoint to see participant submissions - ---- - -## ⏱️ Workshop Timeline - -| Time | Duration | Activity | File | -|------|----------|----------|------| -| 00:00-00:10 | 10 min | Introduction & Architecture | WORKSHOP_INTRO.md | -| 00:10-00:40 | 30 min | Exercise 1: SimpleJson Format | WORKSHOP_GUIDE.md | -| 00:40-01:10 | 30 min | Exercise 2: DirectionsConnector | WORKSHOP_GUIDE.md | -| 01:10-01:25 | 15 min | Testing & Live Demo | WORKSHOP_GUIDE.md | -| 01:25-01:30 | 5 min | Wrap-up & Q&A | - | - ---- - -## πŸ“‹ Prerequisites - -### Required -- Business Central development environment (Sandbox or Docker) -- VS Code with AL Language extension -- Basic AL programming knowledge -- API Base URL (provided by instructor) - -### Helpful -- Understanding of REST APIs -- JSON format familiarity -- Postman or similar API testing tool - ---- - -## πŸŽ“ Learning Objectives - -By the end of this workshop, participants will be able to: - -1. βœ… Understand the E-Document Core framework architecture -2. βœ… Implement the "E-Document" interface for custom formats -3. βœ… Implement IDocumentSender and IDocumentReceiver interfaces -4. βœ… Configure and use E-Document Services in Business Central -5. βœ… Test complete document round-trips (send and receive) -6. βœ… Troubleshoot common integration issues -7. βœ… Know where to find resources for further learning - ---- - -## πŸ“š Key Concepts - -### E-Document Format Interface -Converts Business Central documents to/from external formats: -- **Outgoing**: `Check()`, `Create()`, `CreateBatch()` -- **Incoming**: `GetBasicInfoFromReceivedDocument()`, `GetCompleteInfoFromReceivedDocument()` - -### Integration Interfaces -Communicates with external systems: -- **IDocumentSender**: `Send()` - Send documents -- **IDocumentReceiver**: `ReceiveDocuments()`, `DownloadDocument()` - Receive documents -- **IDocumentResponseHandler**: `GetResponse()` - Async status (advanced) -- **ISentDocumentActions**: Approval/cancellation workflows (advanced) - -### SimpleJson Format -- Simple JSON structure for learning -- Header fields: document type, number, customer, date, amount -- Lines array: line items with quantities and prices -- Human-readable and easy to debug - -### DirectionsConnector -- REST API integration via HTTP -- Authentication via API key header -- Queue-based document exchange -- Stateless and scalable - ---- - -## πŸ§ͺ Testing Scenarios - -### Solo Testing -1. Send documents from your BC instance -2. Verify in API queue via `/peek` -3. Receive documents back into BC -4. Create purchase invoices - -### Partner Testing -1. Partner A sends documents -2. Partner B receives and processes -3. Swap roles and repeat -4. Great for testing interoperability! - -### Group Testing -- Multiple participants send to same queue -- Everyone receives mixed documents -- Tests error handling and validation - ---- - -## πŸ› Troubleshooting - -See the **Troubleshooting** section in `WORKSHOP_GUIDE.md` for: -- Connection issues -- Authentication problems -- JSON parsing errors -- Document creation failures - -Common solutions provided for all scenarios! - ---- - -## πŸ“– Additional Resources - -### Workshop Materials -- [Workshop Introduction](WORKSHOP_INTRO.md) - Slide deck presentation -- [Workshop Guide](WORKSHOP_GUIDE.md) - Detailed instructions with code -- [API Reference](API_REFERENCE.md) - Complete endpoint documentation -- [Workshop Plan](WORKSHOP_PLAN.md) - Implementation overview - -### E-Document Framework -- [E-Document Core README](../../NAV_1/App/BCApps/src/Apps/W1/EDocument/App/README.md) - Official framework documentation -- [E-Document Interface](../../NAV_1/App/BCApps/src/Apps/W1/EDocument/App/src/Document/Interfaces/EDocument.Interface.al) - Interface source code - -### External Resources -- [Business Central Documentation](https://learn.microsoft.com/dynamics365/business-central/) -- [AL Language Reference](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-reference-overview) -- [FastAPI Documentation](https://fastapi.tiangolo.com/) - For the server implementation - ---- - -## 🎁 Bonus Content - -After completing the workshop, try these challenges: - -### Format Enhancements -- Add support for Credit Memos -- Implement batch processing (`CreateBatch()`) -- Add custom field mappings -- Support attachments (PDF, XML) - -### Integration Enhancements -- Implement `IDocumentResponseHandler` for async status -- Add retry logic and error handling -- Implement `ISentDocumentActions` for approvals -- Add custom actions with `IDocumentAction` - -### Real-World Applications -- Connect to actual PEPPOL networks -- Integrate with Avalara or other tax services -- Build EDI integrations -- Create custom XML formats for local requirements - ---- - -## 🀝 Contributing - -This workshop is part of the BCTech repository. If you have suggestions or improvements: - -1. Open an issue on GitHub -2. Submit a pull request -3. Share your feedback with the instructor - ---- - -## πŸ“„ License - -This workshop material is provided under the MIT License. See the repository root for details. - ---- - -## πŸ™ Acknowledgments - -- **Microsoft Business Central Team** - For the E-Document Core framework -- **BCTech Community** - For continuous contributions -- **Workshop Participants** - For your enthusiasm and feedback! - ---- - -## πŸ“ž Support - -### During the Workshop -- Ask the instructor -- Check the WORKSHOP_GUIDE.md -- Collaborate with neighbors - -### After the Workshop -- GitHub Issues: [BCTech Repository](https://github.com/microsoft/BCTech) -- Documentation: [Learn Microsoft](https://learn.microsoft.com/dynamics365/business-central/) -- Community: [Business Central Forums](https://community.dynamics.com/forums/thread/) - ---- - -## 🎯 Quick Start - -**Ready to begin? Follow these steps:** - -1. βœ… Open `WORKSHOP_INTRO.md` and read through the introduction -2. βœ… Get the API Base URL from your instructor -3. βœ… Open `WORKSHOP_GUIDE.md` and start Exercise 1 -4. βœ… Have fun building your E-Document integration! - ---- - -**Happy Coding!** πŸš€ - -*For questions or feedback, contact the workshop instructor.* +# E-Document Connector Workshop +## Directions EMEA 2025 + +Welcome to the E-Document Connector Workshop! This hands-on workshop teaches you how to build complete E-Document integrations using the Business Central E-Document Core framework. + +--- + +## 🎯 Workshop Overview + +**Duration**: 90 minutes (10 min intro + 80 min hands-on) + +**What You'll Build**: +1. **SimpleJson Format** - E-Document format implementation for JSON documents +2. **DirectionsConnector** - HTTP API integration for sending/receiving documents +3. **Complete Round-Trip** - Full document exchange between Business Central and external systems + +**What You'll Learn**: +- How the E-Document Core framework works +- How to implement format interfaces +- How to implement integration interfaces +- Best practices for E-Document solutions + +--- + +## πŸ“‚ Workshop Structure + +``` +DirectionsEMEA2025/ +β”œβ”€β”€ WORKSHOP_INTRO.md # πŸ“Š VS Code presentation (10-minute intro) +β”œβ”€β”€ WORKSHOP_GUIDE.md # πŸ“˜ Step-by-step exercises with solutions +β”œβ”€β”€ API_REFERENCE.md # πŸ”Œ Complete API documentation +β”œβ”€β”€ WORKSHOP_PLAN.md # πŸ“ Detailed implementation plan +β”‚ +β”œβ”€β”€ application/ +β”‚ β”œβ”€β”€ simple_json/ # Exercise 1: Format Extension +β”‚ β”‚ β”œβ”€β”€ app.json +β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al +β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al # ⚠️ TODO sections +β”‚ β”‚ └── SimpleJsonHelper.Codeunit.al # βœ… Pre-written +β”‚ β”‚ +β”‚ └── directions_connector/ # Exercise 2: Integration Extension +β”‚ β”œβ”€β”€ app.json +β”‚ β”œβ”€β”€ DirectionsIntegration.EnumExt.al +β”‚ β”œβ”€β”€ DirectionsIntegration.Codeunit.al # ⚠️ TODO sections +β”‚ β”œβ”€β”€ DirectionsConnectionSetup.Table.al # βœ… Pre-written +β”‚ β”œβ”€β”€ DirectionsConnectionSetup.Page.al # βœ… Pre-written +β”‚ β”œβ”€β”€ DirectionsAuth.Codeunit.al # βœ… Pre-written +β”‚ └── DirectionsRequests.Codeunit.al # βœ… Pre-written +β”‚ +β”œβ”€β”€ solution/ # πŸ“¦ Complete working solution (instructor) +β”‚ β”œβ”€β”€ simple_json/ +β”‚ └── directions_connector/ +β”‚ +└── server/ # 🐍 Python API server (Azure hosted) + β”œβ”€β”€ server.py + β”œβ”€β”€ requirements.txt + └── README.md +``` + +--- + +## πŸš€ Getting Started + +### For Participants + +1. **Read the Introduction** - Open `WORKSHOP_INTRO.md` in VS Code (Ctrl+Shift+V for preview) +2. **Follow the Guide** - Use `WORKSHOP_GUIDE.md` for step-by-step instructions +3. **Reference the API** - Check `API_REFERENCE.md` for endpoint details +4. **Ask Questions** - The instructor is here to help! + +### For Instructors + +1. **Review the Plan** - See `WORKSHOP_PLAN.md` for complete overview +2. **Present the Intro** - Use `WORKSHOP_INTRO.md` as your slide deck in VS Code +3. **Provide API URL** - Update the API Base URL in materials before workshop +4. **Reference Solution** - Complete implementation in `/solution/` folder +5. **Monitor Progress** - Check `/peek` endpoint to see participant submissions + +--- + +## ⏱️ Workshop Timeline + +| Time | Duration | Activity | File | +|------|----------|----------|------| +| 00:00-00:10 | 10 min | Introduction & Architecture | WORKSHOP_INTRO.md | +| 00:10-00:40 | 30 min | Exercise 1: SimpleJson Format | WORKSHOP_GUIDE.md | +| 00:40-01:10 | 30 min | Exercise 2: DirectionsConnector | WORKSHOP_GUIDE.md | +| 01:10-01:25 | 15 min | Testing & Live Demo | WORKSHOP_GUIDE.md | +| 01:25-01:30 | 5 min | Wrap-up & Q&A | - | + +--- + +## πŸ“‹ Prerequisites + +### Required +- Business Central development environment (Sandbox or Docker) +- VS Code with AL Language extension +- Basic AL programming knowledge +- API Base URL (provided by instructor) + +### Helpful +- Understanding of REST APIs +- JSON format familiarity +- Postman or similar API testing tool + +--- + +## πŸŽ“ Learning Objectives + +By the end of this workshop, participants will be able to: + +1. βœ… Understand the E-Document Core framework architecture +2. βœ… Implement the "E-Document" interface for custom formats +3. βœ… Implement IDocumentSender and IDocumentReceiver interfaces +4. βœ… Configure and use E-Document Services in Business Central +5. βœ… Test complete document round-trips (send and receive) +6. βœ… Troubleshoot common integration issues +7. βœ… Know where to find resources for further learning + +--- + +## πŸ“š Key Concepts + +### E-Document Format Interface +Converts Business Central documents to/from external formats: +- **Outgoing**: `Check()`, `Create()`, `CreateBatch()` +- **Incoming**: `GetBasicInfoFromReceivedDocument()`, `GetCompleteInfoFromReceivedDocument()` + +### Integration Interfaces +Communicates with external systems: +- **IDocumentSender**: `Send()` - Send documents +- **IDocumentReceiver**: `ReceiveDocuments()`, `DownloadDocument()` - Receive documents +- **IDocumentResponseHandler**: `GetResponse()` - Async status (advanced) +- **ISentDocumentActions**: Approval/cancellation workflows (advanced) + +### SimpleJson Format +- Simple JSON structure for learning +- Header fields: document type, number, customer, date, amount +- Lines array: line items with quantities and prices +- Human-readable and easy to debug + +### DirectionsConnector +- REST API integration via HTTP +- Authentication via API key header +- Queue-based document exchange +- Stateless and scalable + +--- + +## πŸ§ͺ Testing Scenarios + +### Solo Testing +1. Send documents from your BC instance +2. Verify in API queue via `/peek` +3. Receive documents back into BC +4. Create purchase invoices + +### Partner Testing +1. Partner A sends documents +2. Partner B receives and processes +3. Swap roles and repeat +4. Great for testing interoperability! + +### Group Testing +- Multiple participants send to same queue +- Everyone receives mixed documents +- Tests error handling and validation + +--- + +## πŸ› Troubleshooting + +See the **Troubleshooting** section in `WORKSHOP_GUIDE.md` for: +- Connection issues +- Authentication problems +- JSON parsing errors +- Document creation failures + +Common solutions provided for all scenarios! + +--- + +## πŸ“– Additional Resources + +### Workshop Materials +- [Workshop Introduction](WORKSHOP_INTRO.md) - Slide deck presentation +- [Workshop Guide](WORKSHOP_GUIDE.md) - Detailed instructions with code +- [API Reference](API_REFERENCE.md) - Complete endpoint documentation +- [Workshop Plan](WORKSHOP_PLAN.md) - Implementation overview + +### E-Document Framework +- [E-Document Core README](../../NAV_1/App/BCApps/src/Apps/W1/EDocument/App/README.md) - Official framework documentation +- [E-Document Interface](../../NAV_1/App/BCApps/src/Apps/W1/EDocument/App/src/Document/Interfaces/EDocument.Interface.al) - Interface source code + +### External Resources +- [Business Central Documentation](https://learn.microsoft.com/dynamics365/business-central/) +- [AL Language Reference](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-reference-overview) +- [FastAPI Documentation](https://fastapi.tiangolo.com/) - For the server implementation + +--- + +## 🎁 Bonus Content + +After completing the workshop, try these challenges: + +### Format Enhancements +- Add support for Credit Memos +- Implement batch processing (`CreateBatch()`) +- Add custom field mappings +- Support attachments (PDF, XML) + +### Integration Enhancements +- Implement `IDocumentResponseHandler` for async status +- Add retry logic and error handling +- Implement `ISentDocumentActions` for approvals +- Add custom actions with `IDocumentAction` + +### Real-World Applications +- Connect to actual PEPPOL networks +- Integrate with Avalara or other tax services +- Build EDI integrations +- Create custom XML formats for local requirements + +--- + +## 🀝 Contributing + +This workshop is part of the BCTech repository. If you have suggestions or improvements: + +1. Open an issue on GitHub +2. Submit a pull request +3. Share your feedback with the instructor + +--- + +## πŸ“„ License + +This workshop material is provided under the MIT License. See the repository root for details. + +--- + +## πŸ™ Acknowledgments + +- **Microsoft Business Central Team** - For the E-Document Core framework +- **BCTech Community** - For continuous contributions +- **Workshop Participants** - For your enthusiasm and feedback! + +--- + +## πŸ“ž Support + +### During the Workshop +- Ask the instructor +- Check the WORKSHOP_GUIDE.md +- Collaborate with neighbors + +### After the Workshop +- GitHub Issues: [BCTech Repository](https://github.com/microsoft/BCTech) +- Documentation: [Learn Microsoft](https://learn.microsoft.com/dynamics365/business-central/) +- Community: [Business Central Forums](https://community.dynamics.com/forums/thread/) + +--- + +## 🎯 Quick Start + +**Ready to begin? Follow these steps:** + +1. βœ… Open `WORKSHOP_INTRO.md` and read through the introduction +2. βœ… Get the API Base URL from your instructor +3. βœ… Open `WORKSHOP_GUIDE.md` and start Exercise 1 +4. βœ… Have fun building your E-Document integration! + +--- + +**Happy Coding!** πŸš€ + +*For questions or feedback, contact the workshop instructor.* diff --git a/samples/EDocument/DirectionsEMEA2025/WORKSHOP_GUIDE.md b/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_GUIDE.md similarity index 97% rename from samples/EDocument/DirectionsEMEA2025/WORKSHOP_GUIDE.md rename to samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_GUIDE.md index cbcd304e..48741b56 100644 --- a/samples/EDocument/DirectionsEMEA2025/WORKSHOP_GUIDE.md +++ b/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_GUIDE.md @@ -1,601 +1,601 @@ -# Directions EMEA 2025 - E-Document Connector Workshop - -## Workshop Guide - -Welcome to the E-Document Connector Workshop! In this hands-on session, you'll build a complete E-Document solution that integrates Business Central with an external API using the E-Document Core framework. - ---- - -## 🎯 What You'll Build - -By the end of this workshop, you will have: -1. **SimpleJson Format** - Convert Sales Invoices to JSON and parse incoming Purchase Invoices -2. **DirectionsConnector** - Send and receive documents via HTTP API -3. **Complete Integration** - Full round-trip document exchange - ---- - -## ⏱️ Timeline - -- **Exercise 1** (30 min): Implement SimpleJson Format -- **Exercise 2** (30 min): Implement DirectionsConnector -- **Testing** (15 min): End-to-end validation - ---- - -## πŸ“‹ Prerequisites - -### Required -- Business Central environment (Sandbox or Docker) -- VS Code with AL Language extension -- API Base URL: `[Provided by instructor]` - -### Workshop Files -Your workspace contains: -``` -application/ - β”œβ”€β”€ simple_json/ # Exercise 1 - └── directions_connector/ # Exercise 2 -``` - ---- - -## πŸš€ Exercise 1: SimpleJson Format (30 minutes) - -In this exercise, you'll implement the **"E-Document" interface** to convert Business Central documents to/from JSON format. - -### Part A: Validate Outgoing Documents (5 minutes) - -**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` - -**Find**: The `Check()` procedure (around line 27) - -**Task**: Add validation to ensure required fields are filled before creating the document. - -**Implementation**: -```al -procedure Check(var SourceDocumentHeader: RecordRef; EDocumentService: Record "E-Document Service"; EDocumentProcessingPhase: Enum "E-Document Processing Phase") -var - SalesInvoiceHeader: Record "Sales Invoice Header"; -begin - case SourceDocumentHeader.Number of - Database::"Sales Invoice Header": - begin - // Validate Customer No. is filled - SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Sell-to Customer No.")).TestField(); - - // Validate Posting Date is filled - SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Posting Date")).TestField(); - end; - end; -end; -``` - -**βœ… Validation**: Try posting a Sales Invoice - it should validate required fields. - ---- - -### Part B: Create JSON from Sales Invoice (15 minutes) - -**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` - -**Find**: The `CreateSalesInvoiceJson()` procedure (around line 93) - -**Task**: Generate JSON representation of a Sales Invoice with header and lines. - -**Implementation**: -```al -local procedure CreateSalesInvoiceJson(var SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; var OutStr: OutStream) -var - SalesInvoiceHeader: Record "Sales Invoice Header"; - SalesInvoiceLine: Record "Sales Invoice Line"; - RootObject: JsonObject; - LinesArray: JsonArray; - LineObject: JsonObject; - JsonText: Text; -begin - // Get the actual records from RecordRef - SourceDocumentHeader.SetTable(SalesInvoiceHeader); - SourceDocumentLines.SetTable(SalesInvoiceLine); - - // Add header fields to JSON object - RootObject.Add('documentType', 'Invoice'); - RootObject.Add('documentNo', SalesInvoiceHeader."No."); - RootObject.Add('customerNo', SalesInvoiceHeader."Sell-to Customer No."); - RootObject.Add('customerName', SalesInvoiceHeader."Sell-to Customer Name"); - RootObject.Add('postingDate', Format(SalesInvoiceHeader."Posting Date", 0, '--')); - RootObject.Add('currencyCode', SalesInvoiceHeader."Currency Code"); - RootObject.Add('totalAmount', SalesInvoiceHeader."Amount Including VAT"); - - // Create lines array - if SalesInvoiceLine.FindSet() then - repeat - // Create line object - Clear(LineObject); - LineObject.Add('lineNo', SalesInvoiceLine."Line No."); - LineObject.Add('type', Format(SalesInvoiceLine.Type)); - LineObject.Add('no', SalesInvoiceLine."No."); - LineObject.Add('description', SalesInvoiceLine.Description); - LineObject.Add('quantity', SalesInvoiceLine.Quantity); - LineObject.Add('unitPrice', SalesInvoiceLine."Unit Price"); - LineObject.Add('lineAmount', SalesInvoiceLine."Amount Including VAT"); - LinesArray.Add(LineObject); - until SalesInvoiceLine.Next() = 0; - - // Add lines array to root object - RootObject.Add('lines', LinesArray); - - // Write JSON to stream - RootObject.WriteTo(JsonText); - OutStr.WriteText(JsonText); -end; -``` - -**βœ… Validation**: -1. Create and post a Sales Invoice -2. Open the E-Document list -3. View the E-Document and check the JSON content in the log - ---- - -### Part C: Parse Incoming JSON (Basic Info) (5 minutes) - -**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` - -**Find**: The `GetBasicInfoFromReceivedDocument()` procedure (around line 151) - -**Task**: Extract basic information from incoming JSON to populate E-Document fields. - -**Implementation**: -```al -procedure GetBasicInfoFromReceivedDocument(var EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob") -var - JsonObject: JsonObject; - JsonToken: JsonToken; - SimpleJsonHelper: Codeunit "SimpleJson Helper"; -begin - // Parse JSON from blob - if not SimpleJsonHelper.ReadJsonFromBlob(TempBlob, JsonObject) then - Error('Failed to parse JSON document'); - - // Set document type to Purchase Invoice - EDocument."Document Type" := EDocument."Document Type"::"Purchase Invoice"; - - // Extract document number - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'documentNo', JsonToken) then - EDocument."Document No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Document No.")); - - // Extract vendor number (from customerNo in JSON) - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerNo', JsonToken) then - EDocument."Bill-to/Pay-to No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Bill-to/Pay-to No.")); - - // Extract vendor name - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerName', JsonToken) then - EDocument."Bill-to/Pay-to Name" := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Bill-to/Pay-to Name")); - - // Extract posting date - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then - EDocument."Document Date" := SimpleJsonHelper.GetJsonTokenDate(JsonToken); - - // Extract currency code - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'currencyCode', JsonToken) then - EDocument."Currency Code" := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Currency Code")); -end; -``` - -**βœ… Validation**: This will be tested in Exercise 2 when receiving documents. - ---- - -### Part D: Create Purchase Invoice from JSON (5 minutes) - -**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` - -**Find**: The `GetCompleteInfoFromReceivedDocument()` procedure (around line 188) - -**Task**: Create a Purchase Invoice record from JSON data. - -**Implementation**: -```al -procedure GetCompleteInfoFromReceivedDocument(var EDocument: Record "E-Document"; var CreatedDocumentHeader: RecordRef; var CreatedDocumentLines: RecordRef; var TempBlob: Codeunit "Temp Blob") -var - PurchaseHeader: Record "Purchase Header"; - PurchaseLine: Record "Purchase Line"; - JsonObject: JsonObject; - JsonToken: JsonToken; - JsonArray: JsonArray; - JsonLineToken: JsonToken; - SimpleJsonHelper: Codeunit "SimpleJson Helper"; - LineNo: Integer; -begin - // Parse JSON from blob - if not SimpleJsonHelper.ReadJsonFromBlob(TempBlob, JsonObject) then - Error('Failed to parse JSON document'); - - // Create Purchase Header - PurchaseHeader.Init(); - PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::Invoice; - PurchaseHeader.Insert(true); - - // Set vendor from JSON - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerNo', JsonToken) then - PurchaseHeader.Validate("Buy-from Vendor No.", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); - - // Set posting date - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then - PurchaseHeader.Validate("Posting Date", SimpleJsonHelper.GetJsonTokenDate(JsonToken)); - - // Set currency code (if not blank) - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'currencyCode', JsonToken) then begin - if SimpleJsonHelper.GetJsonTokenValue(JsonToken) <> '' then - PurchaseHeader.Validate("Currency Code", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); - end; - - PurchaseHeader.Modify(true); - - // Create Purchase Lines from JSON array - if JsonObject.Get('lines', JsonToken) then begin - JsonArray := JsonToken.AsArray(); - LineNo := 10000; - - foreach JsonLineToken in JsonArray do begin - JsonObject := JsonLineToken.AsObject(); - - PurchaseLine.Init(); - PurchaseLine."Document Type" := PurchaseHeader."Document Type"; - PurchaseLine."Document No." := PurchaseHeader."No."; - PurchaseLine."Line No." := LineNo; - - // Set type (default to Item) - PurchaseLine.Type := PurchaseLine.Type::Item; - - // Set item number - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'no', JsonToken) then - PurchaseLine.Validate("No.", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); - - // Set description - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'description', JsonToken) then - PurchaseLine.Description := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(PurchaseLine.Description)); - - // Set quantity - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'quantity', JsonToken) then - PurchaseLine.Validate(Quantity, SimpleJsonHelper.GetJsonTokenDecimal(JsonToken)); - - // Set unit price - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'unitPrice', JsonToken) then - PurchaseLine.Validate("Direct Unit Cost", SimpleJsonHelper.GetJsonTokenDecimal(JsonToken)); - - PurchaseLine.Insert(true); - LineNo += 10000; - end; - end; - - // Return via RecordRef - CreatedDocumentHeader.GetTable(PurchaseHeader); - CreatedDocumentLines.GetTable(PurchaseLine); -end; -``` - -**βœ… Validation**: This will be tested in Exercise 2 when creating documents from received E-Documents. - ---- - -## πŸ”Œ Exercise 2: DirectionsConnector (30 minutes) - -In this exercise, you'll implement the **IDocumentSender** and **IDocumentReceiver** interfaces to send/receive documents via HTTP API. - -### Part A: Setup Connection (5 minutes) - -**Manual Setup**: -1. Open Business Central -2. Search for "Directions Connection Setup" -3. Enter the API Base URL: `[Provided by instructor]` -4. Enter your name (unique identifier) -5. Click "Register" to get your API key -6. Click "Test Connection" to verify - -**βœ… Validation**: You should see "Connection test successful!" - ---- - -### Part B: Send Document (10 minutes) - -**File**: `application/directions_connector/DirectionsIntegration.Codeunit.al` - -**Find**: The `Send()` procedure (around line 31) - -**Task**: Send an E-Document to the API /enqueue endpoint. - -**Implementation**: -```al -procedure Send(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; SendContext: Codeunit SendContext) -var - DirectionsSetup: Record "Directions Connection Setup"; - DirectionsAuth: Codeunit "Directions Auth"; - DirectionsRequests: Codeunit "Directions Requests"; - HttpClient: HttpClient; - HttpRequest: HttpRequestMessage; - HttpResponse: HttpResponseMessage; - TempBlob: Codeunit "Temp Blob"; - JsonContent: Text; -begin - // Get connection setup - DirectionsAuth.GetConnectionSetup(DirectionsSetup); - - // Get document content from SendContext - SendContext.GetTempBlob(TempBlob); - JsonContent := DirectionsRequests.ReadJsonFromBlob(TempBlob); - - // Create POST request to /enqueue - DirectionsRequests.CreatePostRequest(DirectionsSetup."API Base URL" + 'enqueue', JsonContent, HttpRequest); - - // Add authentication header - DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); - - // Log request (for E-Document framework logging) - SendContext.Http().SetHttpRequestMessage(HttpRequest); - - // Send request - if not HttpClient.Send(HttpRequest, HttpResponse) then - Error('Failed to send document to API.'); - - // Log response and check success - SendContext.Http().SetHttpResponseMessage(HttpResponse); - DirectionsRequests.CheckResponseSuccess(HttpResponse); -end; -``` - -**βœ… Validation**: -1. Create and post a Sales Invoice -2. Open the E-Document list -3. Send the E-Document (it should succeed) -4. Use the /peek endpoint in a browser to verify the document is in the queue - ---- - -### Part C: Receive Documents List (10 minutes) - -**File**: `application/directions_connector/DirectionsIntegration.Codeunit.al` - -**Find**: The `ReceiveDocuments()` procedure (around line 72) - -**Task**: Retrieve the list of available documents from the API /peek endpoint. - -**Implementation**: -```al -procedure ReceiveDocuments(var EDocumentService: Record "E-Document Service"; DocumentsMetadata: Codeunit "Temp Blob List"; ReceiveContext: Codeunit ReceiveContext) -var - DirectionsSetup: Record "Directions Connection Setup"; - DirectionsAuth: Codeunit "Directions Auth"; - DirectionsRequests: Codeunit "Directions Requests"; - HttpClient: HttpClient; - HttpRequest: HttpRequestMessage; - HttpResponse: HttpResponseMessage; - JsonObject: JsonObject; - JsonToken: JsonToken; - JsonArray: JsonArray; - TempBlob: Codeunit "Temp Blob"; - ResponseText: Text; - DocumentJson: Text; -begin - // Get connection setup - DirectionsAuth.GetConnectionSetup(DirectionsSetup); - - // Create GET request to /peek - DirectionsRequests.CreateGetRequest(DirectionsSetup."API Base URL" + 'peek', HttpRequest); - - // Add authentication - DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); - - // Log request - ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); - - // Send request - if not HttpClient.Send(HttpRequest, HttpResponse) then - Error('Failed to retrieve documents from API.'); - - // Log response and check success - ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); - DirectionsRequests.CheckResponseSuccess(HttpResponse); - - // Parse response and extract documents - ResponseText := DirectionsRequests.GetResponseText(HttpResponse); - if JsonObject.ReadFrom(ResponseText) then begin - if JsonObject.Get('items', JsonToken) then begin - JsonArray := JsonToken.AsArray(); - foreach JsonToken in JsonArray do begin - // Create a TempBlob for each document metadata - Clear(TempBlob); - JsonToken.WriteTo(DocumentJson); - DirectionsRequests.WriteTextToBlob(DocumentJson, TempBlob); - DocumentsMetadata.Add(TempBlob); - end; - end; - end; -end; -``` - -**βœ… Validation**: This will be tested when running "Get Documents" action in BC. - ---- - -### Part D: Download Single Document (5 minutes) - -**File**: `application/directions_connector/DirectionsIntegration.Codeunit.al` - -**Find**: The `DownloadDocument()` procedure (around line 135) - -**Task**: Download a single document from the API /dequeue endpoint. - -**Implementation**: -```al -procedure DownloadDocument(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; DocumentMetadata: codeunit "Temp Blob"; ReceiveContext: Codeunit ReceiveContext) -var - DirectionsSetup: Record "Directions Connection Setup"; - DirectionsAuth: Codeunit "Directions Auth"; - DirectionsRequests: Codeunit "Directions Requests"; - HttpClient: HttpClient; - HttpRequest: HttpRequestMessage; - HttpResponse: HttpResponseMessage; - JsonObject: JsonObject; - JsonToken: JsonToken; - TempBlob: Codeunit "Temp Blob"; - ResponseText: Text; - DocumentJson: Text; -begin - // Get connection setup - DirectionsAuth.GetConnectionSetup(DirectionsSetup); - - // Create GET request to /dequeue - DirectionsRequests.CreateGetRequest(DirectionsSetup."API Base URL" + 'dequeue', HttpRequest); - - // Add authentication - DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); - - // Log request - ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); - - // Send request - if not HttpClient.Send(HttpRequest, HttpResponse) then - Error('Failed to download document from API.'); - - // Log response and check success - ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); - DirectionsRequests.CheckResponseSuccess(HttpResponse); - - // Parse response and extract document - ResponseText := DirectionsRequests.GetResponseText(HttpResponse); - if JsonObject.ReadFrom(ResponseText) then begin - if JsonObject.Get('document', JsonToken) then begin - JsonToken.WriteTo(DocumentJson); - ReceiveContext.GetTempBlob(TempBlob); - DirectionsRequests.WriteTextToBlob(DocumentJson, TempBlob); - end else - Error('No document found in response.'); - end; -end; -``` - -**βœ… Validation**: This will be tested in the complete flow below. - ---- - -## πŸ§ͺ Testing - Complete Flow (15 minutes) - -### Setup E-Document Service - -1. Open "E-Document Services" -2. Create a new service: - - **Code**: DIRECTIONS - - **Description**: Directions Connector - - **Document Format**: Simple JSON Format - - **Service Integration**: Directions Connector -3. Click "Setup Service Integration" and verify your connection -4. Enable the service - -### Test Outgoing Flow - -1. **Create Sales Invoice**: - - Customer: Any customer - - Add at least one line item - - Post the invoice - -2. **Send E-Document**: - - Open "E-Documents" list - - Find your posted invoice - - Action: "Send" - - Status should change to "Sent" - -3. **Verify in API**: - - Open browser: `[API Base URL]/peek` - - Add header: `X-Service-Key: [Your API Key]` - - You should see your document in the "items" array - -### Test Incoming Flow - -1. **Receive Documents**: - - Open "E-Document Services" - - Select your DIRECTIONS service - - Action: "Get Documents" - - New E-Documents should appear with status "Imported" - -2. **View Received Document**: - - Open the received E-Document - - Check the JSON content in the log - - Verify basic info is populated (vendor, date, etc.) - -3. **Create Purchase Invoice**: - - Action: "Create Document" - - A Purchase Invoice should be created - - Open the Purchase Invoice and verify: - - Vendor is set correctly - - Lines are populated with items, quantities, prices - - Dates and currency match - -4. **Verify Queue**: - - Check /peek endpoint again - - The queue should be empty (documents were dequeued) - ---- - -## πŸŽ‰ Success Criteria - -You have successfully completed the workshop if: -- βœ… You can post a Sales Invoice and see it as an E-Document -- βœ… The E-Document contains valid JSON -- βœ… You can send the E-Document to the API -- βœ… The document appears in the API queue -- βœ… You can receive documents from the API -- βœ… Purchase Invoices are created from received documents -- βœ… All data is mapped correctly - ---- - -## πŸ› Troubleshooting - -### "Failed to connect to API" -- Check the API Base URL in setup -- Verify the API server is running -- Check firewall settings - -### "Unauthorized or invalid key" -- Re-register in the setup page -- Verify the API key is saved -- Check that you're using the correct key header - -### "Document type not supported" -- Verify you selected "Simple JSON Format" in E-Document Service -- Check the format enum extension is compiled - -### "Failed to parse JSON" -- Check the JSON structure in E-Document log -- Verify all required fields are present -- Look for syntax errors (missing commas, brackets) - -### "Vendor does not exist" -- Create a vendor with the same number as the customer in the JSON -- Or modify the JSON to use an existing vendor number - ---- - -## πŸ“š Additional Resources - -- [E-Document Core README](../../README.md) -- [API Reference](../API_REFERENCE.md) -- [Workshop Plan](../WORKSHOP_PLAN.md) - ---- - -## πŸŽ“ Homework / Advanced Exercises - -Want to learn more? Try these: -1. Add support for Credit Memos -2. Implement `GetResponse()` for async status checking -3. Add custom fields to the JSON format -4. Implement validation rules for incoming documents -5. Add error handling and retry logic -6. Create a batch send function - ---- - -**Congratulations!** 🎊 You've completed the E-Document Connector Workshop! +# Directions EMEA 2025 - E-Document Connector Workshop + +## Workshop Guide + +Welcome to the E-Document Connector Workshop! In this hands-on session, you'll build a complete E-Document solution that integrates Business Central with an external API using the E-Document Core framework. + +--- + +## 🎯 What You'll Build + +By the end of this workshop, you will have: +1. **SimpleJson Format** - Convert Sales Invoices to JSON and parse incoming Purchase Invoices +2. **DirectionsConnector** - Send and receive documents via HTTP API +3. **Complete Integration** - Full round-trip document exchange + +--- + +## ⏱️ Timeline + +- **Exercise 1** (30 min): Implement SimpleJson Format +- **Exercise 2** (30 min): Implement DirectionsConnector +- **Testing** (15 min): End-to-end validation + +--- + +## πŸ“‹ Prerequisites + +### Required +- Business Central environment (Sandbox or Docker) +- VS Code with AL Language extension +- API Base URL: `[Provided by instructor]` + +### Workshop Files +Your workspace contains: +``` +application/ + β”œβ”€β”€ simple_json/ # Exercise 1 + └── directions_connector/ # Exercise 2 +``` + +--- + +## πŸš€ Exercise 1: SimpleJson Format (30 minutes) + +In this exercise, you'll implement the **"E-Document" interface** to convert Business Central documents to/from JSON format. + +### Part A: Validate Outgoing Documents (5 minutes) + +**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` + +**Find**: The `Check()` procedure (around line 27) + +**Task**: Add validation to ensure required fields are filled before creating the document. + +**Implementation**: +```al +procedure Check(var SourceDocumentHeader: RecordRef; EDocumentService: Record "E-Document Service"; EDocumentProcessingPhase: Enum "E-Document Processing Phase") +var + SalesInvoiceHeader: Record "Sales Invoice Header"; +begin + case SourceDocumentHeader.Number of + Database::"Sales Invoice Header": + begin + // Validate Customer No. is filled + SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Sell-to Customer No.")).TestField(); + + // Validate Posting Date is filled + SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Posting Date")).TestField(); + end; + end; +end; +``` + +**βœ… Validation**: Try posting a Sales Invoice - it should validate required fields. + +--- + +### Part B: Create JSON from Sales Invoice (15 minutes) + +**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` + +**Find**: The `CreateSalesInvoiceJson()` procedure (around line 93) + +**Task**: Generate JSON representation of a Sales Invoice with header and lines. + +**Implementation**: +```al +local procedure CreateSalesInvoiceJson(var SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; var OutStr: OutStream) +var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + RootObject: JsonObject; + LinesArray: JsonArray; + LineObject: JsonObject; + JsonText: Text; +begin + // Get the actual records from RecordRef + SourceDocumentHeader.SetTable(SalesInvoiceHeader); + SourceDocumentLines.SetTable(SalesInvoiceLine); + + // Add header fields to JSON object + RootObject.Add('documentType', 'Invoice'); + RootObject.Add('documentNo', SalesInvoiceHeader."No."); + RootObject.Add('customerNo', SalesInvoiceHeader."Sell-to Customer No."); + RootObject.Add('customerName', SalesInvoiceHeader."Sell-to Customer Name"); + RootObject.Add('postingDate', Format(SalesInvoiceHeader."Posting Date", 0, '--')); + RootObject.Add('currencyCode', SalesInvoiceHeader."Currency Code"); + RootObject.Add('totalAmount', SalesInvoiceHeader."Amount Including VAT"); + + // Create lines array + if SalesInvoiceLine.FindSet() then + repeat + // Create line object + Clear(LineObject); + LineObject.Add('lineNo', SalesInvoiceLine."Line No."); + LineObject.Add('type', Format(SalesInvoiceLine.Type)); + LineObject.Add('no', SalesInvoiceLine."No."); + LineObject.Add('description', SalesInvoiceLine.Description); + LineObject.Add('quantity', SalesInvoiceLine.Quantity); + LineObject.Add('unitPrice', SalesInvoiceLine."Unit Price"); + LineObject.Add('lineAmount', SalesInvoiceLine."Amount Including VAT"); + LinesArray.Add(LineObject); + until SalesInvoiceLine.Next() = 0; + + // Add lines array to root object + RootObject.Add('lines', LinesArray); + + // Write JSON to stream + RootObject.WriteTo(JsonText); + OutStr.WriteText(JsonText); +end; +``` + +**βœ… Validation**: +1. Create and post a Sales Invoice +2. Open the E-Document list +3. View the E-Document and check the JSON content in the log + +--- + +### Part C: Parse Incoming JSON (Basic Info) (5 minutes) + +**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` + +**Find**: The `GetBasicInfoFromReceivedDocument()` procedure (around line 151) + +**Task**: Extract basic information from incoming JSON to populate E-Document fields. + +**Implementation**: +```al +procedure GetBasicInfoFromReceivedDocument(var EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob") +var + JsonObject: JsonObject; + JsonToken: JsonToken; + SimpleJsonHelper: Codeunit "SimpleJson Helper"; +begin + // Parse JSON from blob + if not SimpleJsonHelper.ReadJsonFromBlob(TempBlob, JsonObject) then + Error('Failed to parse JSON document'); + + // Set document type to Purchase Invoice + EDocument."Document Type" := EDocument."Document Type"::"Purchase Invoice"; + + // Extract document number + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'documentNo', JsonToken) then + EDocument."Document No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Document No.")); + + // Extract vendor number (from customerNo in JSON) + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerNo', JsonToken) then + EDocument."Bill-to/Pay-to No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Bill-to/Pay-to No.")); + + // Extract vendor name + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerName', JsonToken) then + EDocument."Bill-to/Pay-to Name" := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Bill-to/Pay-to Name")); + + // Extract posting date + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then + EDocument."Document Date" := SimpleJsonHelper.GetJsonTokenDate(JsonToken); + + // Extract currency code + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'currencyCode', JsonToken) then + EDocument."Currency Code" := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Currency Code")); +end; +``` + +**βœ… Validation**: This will be tested in Exercise 2 when receiving documents. + +--- + +### Part D: Create Purchase Invoice from JSON (5 minutes) + +**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` + +**Find**: The `GetCompleteInfoFromReceivedDocument()` procedure (around line 188) + +**Task**: Create a Purchase Invoice record from JSON data. + +**Implementation**: +```al +procedure GetCompleteInfoFromReceivedDocument(var EDocument: Record "E-Document"; var CreatedDocumentHeader: RecordRef; var CreatedDocumentLines: RecordRef; var TempBlob: Codeunit "Temp Blob") +var + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + JsonObject: JsonObject; + JsonToken: JsonToken; + JsonArray: JsonArray; + JsonLineToken: JsonToken; + SimpleJsonHelper: Codeunit "SimpleJson Helper"; + LineNo: Integer; +begin + // Parse JSON from blob + if not SimpleJsonHelper.ReadJsonFromBlob(TempBlob, JsonObject) then + Error('Failed to parse JSON document'); + + // Create Purchase Header + PurchaseHeader.Init(); + PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::Invoice; + PurchaseHeader.Insert(true); + + // Set vendor from JSON + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerNo', JsonToken) then + PurchaseHeader.Validate("Buy-from Vendor No.", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); + + // Set posting date + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then + PurchaseHeader.Validate("Posting Date", SimpleJsonHelper.GetJsonTokenDate(JsonToken)); + + // Set currency code (if not blank) + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'currencyCode', JsonToken) then begin + if SimpleJsonHelper.GetJsonTokenValue(JsonToken) <> '' then + PurchaseHeader.Validate("Currency Code", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); + end; + + PurchaseHeader.Modify(true); + + // Create Purchase Lines from JSON array + if JsonObject.Get('lines', JsonToken) then begin + JsonArray := JsonToken.AsArray(); + LineNo := 10000; + + foreach JsonLineToken in JsonArray do begin + JsonObject := JsonLineToken.AsObject(); + + PurchaseLine.Init(); + PurchaseLine."Document Type" := PurchaseHeader."Document Type"; + PurchaseLine."Document No." := PurchaseHeader."No."; + PurchaseLine."Line No." := LineNo; + + // Set type (default to Item) + PurchaseLine.Type := PurchaseLine.Type::Item; + + // Set item number + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'no', JsonToken) then + PurchaseLine.Validate("No.", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); + + // Set description + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'description', JsonToken) then + PurchaseLine.Description := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(PurchaseLine.Description)); + + // Set quantity + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'quantity', JsonToken) then + PurchaseLine.Validate(Quantity, SimpleJsonHelper.GetJsonTokenDecimal(JsonToken)); + + // Set unit price + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'unitPrice', JsonToken) then + PurchaseLine.Validate("Direct Unit Cost", SimpleJsonHelper.GetJsonTokenDecimal(JsonToken)); + + PurchaseLine.Insert(true); + LineNo += 10000; + end; + end; + + // Return via RecordRef + CreatedDocumentHeader.GetTable(PurchaseHeader); + CreatedDocumentLines.GetTable(PurchaseLine); +end; +``` + +**βœ… Validation**: This will be tested in Exercise 2 when creating documents from received E-Documents. + +--- + +## πŸ”Œ Exercise 2: DirectionsConnector (30 minutes) + +In this exercise, you'll implement the **IDocumentSender** and **IDocumentReceiver** interfaces to send/receive documents via HTTP API. + +### Part A: Setup Connection (5 minutes) + +**Manual Setup**: +1. Open Business Central +2. Search for "Directions Connection Setup" +3. Enter the API Base URL: `[Provided by instructor]` +4. Enter your name (unique identifier) +5. Click "Register" to get your API key +6. Click "Test Connection" to verify + +**βœ… Validation**: You should see "Connection test successful!" + +--- + +### Part B: Send Document (10 minutes) + +**File**: `application/directions_connector/DirectionsIntegration.Codeunit.al` + +**Find**: The `Send()` procedure (around line 31) + +**Task**: Send an E-Document to the API /enqueue endpoint. + +**Implementation**: +```al +procedure Send(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; SendContext: Codeunit SendContext) +var + DirectionsSetup: Record "Directions Connection Setup"; + DirectionsAuth: Codeunit "Directions Auth"; + DirectionsRequests: Codeunit "Directions Requests"; + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + TempBlob: Codeunit "Temp Blob"; + JsonContent: Text; +begin + // Get connection setup + DirectionsAuth.GetConnectionSetup(DirectionsSetup); + + // Get document content from SendContext + SendContext.GetTempBlob(TempBlob); + JsonContent := DirectionsRequests.ReadJsonFromBlob(TempBlob); + + // Create POST request to /enqueue + DirectionsRequests.CreatePostRequest(DirectionsSetup."API Base URL" + 'enqueue', JsonContent, HttpRequest); + + // Add authentication header + DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); + + // Log request (for E-Document framework logging) + SendContext.Http().SetHttpRequestMessage(HttpRequest); + + // Send request + if not HttpClient.Send(HttpRequest, HttpResponse) then + Error('Failed to send document to API.'); + + // Log response and check success + SendContext.Http().SetHttpResponseMessage(HttpResponse); + DirectionsRequests.CheckResponseSuccess(HttpResponse); +end; +``` + +**βœ… Validation**: +1. Create and post a Sales Invoice +2. Open the E-Document list +3. Send the E-Document (it should succeed) +4. Use the /peek endpoint in a browser to verify the document is in the queue + +--- + +### Part C: Receive Documents List (10 minutes) + +**File**: `application/directions_connector/DirectionsIntegration.Codeunit.al` + +**Find**: The `ReceiveDocuments()` procedure (around line 72) + +**Task**: Retrieve the list of available documents from the API /peek endpoint. + +**Implementation**: +```al +procedure ReceiveDocuments(var EDocumentService: Record "E-Document Service"; DocumentsMetadata: Codeunit "Temp Blob List"; ReceiveContext: Codeunit ReceiveContext) +var + DirectionsSetup: Record "Directions Connection Setup"; + DirectionsAuth: Codeunit "Directions Auth"; + DirectionsRequests: Codeunit "Directions Requests"; + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + JsonObject: JsonObject; + JsonToken: JsonToken; + JsonArray: JsonArray; + TempBlob: Codeunit "Temp Blob"; + ResponseText: Text; + DocumentJson: Text; +begin + // Get connection setup + DirectionsAuth.GetConnectionSetup(DirectionsSetup); + + // Create GET request to /peek + DirectionsRequests.CreateGetRequest(DirectionsSetup."API Base URL" + 'peek', HttpRequest); + + // Add authentication + DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); + + // Log request + ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); + + // Send request + if not HttpClient.Send(HttpRequest, HttpResponse) then + Error('Failed to retrieve documents from API.'); + + // Log response and check success + ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); + DirectionsRequests.CheckResponseSuccess(HttpResponse); + + // Parse response and extract documents + ResponseText := DirectionsRequests.GetResponseText(HttpResponse); + if JsonObject.ReadFrom(ResponseText) then begin + if JsonObject.Get('items', JsonToken) then begin + JsonArray := JsonToken.AsArray(); + foreach JsonToken in JsonArray do begin + // Create a TempBlob for each document metadata + Clear(TempBlob); + JsonToken.WriteTo(DocumentJson); + DirectionsRequests.WriteTextToBlob(DocumentJson, TempBlob); + DocumentsMetadata.Add(TempBlob); + end; + end; + end; +end; +``` + +**βœ… Validation**: This will be tested when running "Get Documents" action in BC. + +--- + +### Part D: Download Single Document (5 minutes) + +**File**: `application/directions_connector/DirectionsIntegration.Codeunit.al` + +**Find**: The `DownloadDocument()` procedure (around line 135) + +**Task**: Download a single document from the API /dequeue endpoint. + +**Implementation**: +```al +procedure DownloadDocument(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; DocumentMetadata: codeunit "Temp Blob"; ReceiveContext: Codeunit ReceiveContext) +var + DirectionsSetup: Record "Directions Connection Setup"; + DirectionsAuth: Codeunit "Directions Auth"; + DirectionsRequests: Codeunit "Directions Requests"; + HttpClient: HttpClient; + HttpRequest: HttpRequestMessage; + HttpResponse: HttpResponseMessage; + JsonObject: JsonObject; + JsonToken: JsonToken; + TempBlob: Codeunit "Temp Blob"; + ResponseText: Text; + DocumentJson: Text; +begin + // Get connection setup + DirectionsAuth.GetConnectionSetup(DirectionsSetup); + + // Create GET request to /dequeue + DirectionsRequests.CreateGetRequest(DirectionsSetup."API Base URL" + 'dequeue', HttpRequest); + + // Add authentication + DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); + + // Log request + ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); + + // Send request + if not HttpClient.Send(HttpRequest, HttpResponse) then + Error('Failed to download document from API.'); + + // Log response and check success + ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); + DirectionsRequests.CheckResponseSuccess(HttpResponse); + + // Parse response and extract document + ResponseText := DirectionsRequests.GetResponseText(HttpResponse); + if JsonObject.ReadFrom(ResponseText) then begin + if JsonObject.Get('document', JsonToken) then begin + JsonToken.WriteTo(DocumentJson); + ReceiveContext.GetTempBlob(TempBlob); + DirectionsRequests.WriteTextToBlob(DocumentJson, TempBlob); + end else + Error('No document found in response.'); + end; +end; +``` + +**βœ… Validation**: This will be tested in the complete flow below. + +--- + +## πŸ§ͺ Testing - Complete Flow (15 minutes) + +### Setup E-Document Service + +1. Open "E-Document Services" +2. Create a new service: + - **Code**: DIRECTIONS + - **Description**: Directions Connector + - **Document Format**: Simple JSON Format + - **Service Integration**: Directions Connector +3. Click "Setup Service Integration" and verify your connection +4. Enable the service + +### Test Outgoing Flow + +1. **Create Sales Invoice**: + - Customer: Any customer + - Add at least one line item + - Post the invoice + +2. **Send E-Document**: + - Open "E-Documents" list + - Find your posted invoice + - Action: "Send" + - Status should change to "Sent" + +3. **Verify in API**: + - Open browser: `[API Base URL]/peek` + - Add header: `X-Service-Key: [Your API Key]` + - You should see your document in the "items" array + +### Test Incoming Flow + +1. **Receive Documents**: + - Open "E-Document Services" + - Select your DIRECTIONS service + - Action: "Get Documents" + - New E-Documents should appear with status "Imported" + +2. **View Received Document**: + - Open the received E-Document + - Check the JSON content in the log + - Verify basic info is populated (vendor, date, etc.) + +3. **Create Purchase Invoice**: + - Action: "Create Document" + - A Purchase Invoice should be created + - Open the Purchase Invoice and verify: + - Vendor is set correctly + - Lines are populated with items, quantities, prices + - Dates and currency match + +4. **Verify Queue**: + - Check /peek endpoint again + - The queue should be empty (documents were dequeued) + +--- + +## πŸŽ‰ Success Criteria + +You have successfully completed the workshop if: +- βœ… You can post a Sales Invoice and see it as an E-Document +- βœ… The E-Document contains valid JSON +- βœ… You can send the E-Document to the API +- βœ… The document appears in the API queue +- βœ… You can receive documents from the API +- βœ… Purchase Invoices are created from received documents +- βœ… All data is mapped correctly + +--- + +## πŸ› Troubleshooting + +### "Failed to connect to API" +- Check the API Base URL in setup +- Verify the API server is running +- Check firewall settings + +### "Unauthorized or invalid key" +- Re-register in the setup page +- Verify the API key is saved +- Check that you're using the correct key header + +### "Document type not supported" +- Verify you selected "Simple JSON Format" in E-Document Service +- Check the format enum extension is compiled + +### "Failed to parse JSON" +- Check the JSON structure in E-Document log +- Verify all required fields are present +- Look for syntax errors (missing commas, brackets) + +### "Vendor does not exist" +- Create a vendor with the same number as the customer in the JSON +- Or modify the JSON to use an existing vendor number + +--- + +## πŸ“š Additional Resources + +- [E-Document Core README](../../README.md) +- [API Reference](../API_REFERENCE.md) +- [Workshop Plan](../WORKSHOP_PLAN.md) + +--- + +## πŸŽ“ Homework / Advanced Exercises + +Want to learn more? Try these: +1. Add support for Credit Memos +2. Implement `GetResponse()` for async status checking +3. Add custom fields to the JSON format +4. Implement validation rules for incoming documents +5. Add error handling and retry logic +6. Create a batch send function + +--- + +**Congratulations!** 🎊 You've completed the E-Document Connector Workshop! diff --git a/samples/EDocument/DirectionsEMEA2025/WORKSHOP_INTRO.md b/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_INTRO.md similarity index 96% rename from samples/EDocument/DirectionsEMEA2025/WORKSHOP_INTRO.md rename to samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_INTRO.md index 79fc7697..adc7446d 100644 --- a/samples/EDocument/DirectionsEMEA2025/WORKSHOP_INTRO.md +++ b/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_INTRO.md @@ -1,487 +1,487 @@ -# 🎯 E-Document Connector Workshop -## Directions EMEA 2025 - ---- - -## πŸ‘‹ Welcome! - -In the next **90 minutes**, you'll build a complete E-Document integration solution. - -**What you'll learn:** -- How the E-Document Core framework works -- How to implement format interfaces (JSON) -- How to implement integration interfaces (HTTP API) -- Complete round-trip document exchange - -**What you'll build:** -- βœ… SimpleJson Format - Convert documents to/from JSON -- βœ… DirectionsConnector - Send/receive via HTTP API - ---- - -## πŸ—οΈ Architecture Overview - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Business Central β”‚ -β”‚ β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Sales Invoice│───▢│ E-Document Core β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Format Interface│◀─── You implement! β”‚ -β”‚ β”‚ (SimpleJson) β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ JSON Blob β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Integration Interface │◀─── You implement! β”‚ -β”‚ β”‚ (DirectionsConnector) β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ HTTPS - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Azure API Server β”‚ - β”‚ β”‚ - β”‚ Queue Management β”‚ - β”‚ Document Storage β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Another Company / β”‚ - β”‚ Trading Partner β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - ---- - -## πŸ“¦ E-Document Core Framework - -The framework provides: - -### 1️⃣ **Document Interface** (`"E-Document"`) -Convert documents to/from Business Central - -**Outgoing** (BC β†’ External): -- `Check()` - Validate before sending -- `Create()` - Convert to format (JSON, XML, etc.) -- `CreateBatch()` - Batch multiple documents - -**Incoming** (External β†’ BC): -- `GetBasicInfoFromReceivedDocument()` - Parse metadata -- `GetCompleteInfoFromReceivedDocument()` - Create BC document - -### 2️⃣ **Integration Interfaces** -Send/receive documents via various channels - -**IDocumentSender**: -- `Send()` - Send document to external service - -**IDocumentReceiver**: -- `ReceiveDocuments()` - Get list of available documents -- `DownloadDocument()` - Download specific document - -**Others** (Advanced): -- `IDocumentResponseHandler` - Async status checking -- `ISentDocumentActions` - Approval/cancellation -- `IDocumentAction` - Custom actions - ---- - -## 🎨 What is SimpleJson? - -A simple JSON format for E-Documents designed for this workshop. - -### Example JSON Structure: - -```json -{ - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001", - "customerName": "Contoso Ltd.", - "postingDate": "2025-10-21", - "currencyCode": "USD", - "totalAmount": 1250.00, - "lines": [ - { - "lineNo": 1, - "type": "Item", - "no": "ITEM-001", - "description": "Item A", - "quantity": 5, - "unitPrice": 250.00, - "lineAmount": 1250.00 - } - ] -} -``` - -**Why JSON?** -- βœ… Human-readable -- βœ… Easy to parse -- βœ… Widely supported -- βœ… Perfect for learning - ---- - -## πŸ”Œ What is DirectionsConnector? - -An HTTP-based integration that sends/receives documents via REST API. - -### API Endpoints: - -| Endpoint | Method | Purpose | -|----------|--------|---------| -| `/register` | POST | Get API key | -| `/enqueue` | POST | Send document | -| `/peek` | GET | View queue | -| `/dequeue` | GET | Receive document | -| `/clear` | DELETE | Clear queue | - -### Authentication: -```http -X-Service-Key: your-api-key-here -``` - -### Why HTTP API? -- βœ… Simple and universal -- βœ… Easy to test (browser, Postman) -- βœ… Real-world scenario -- βœ… Stateless and scalable - ---- - -## πŸ”„ Complete Flow - -### Outgoing (Sending): -``` -Sales Invoice (BC) - ↓ Post -E-Document Created - ↓ Format: SimpleJson.Create() -JSON Blob - ↓ Integration: DirectionsConnector.Send() -HTTP POST /enqueue - ↓ -Azure API Queue -``` - -### Incoming (Receiving): -``` -Azure API Queue - ↓ Integration: DirectionsConnector.ReceiveDocuments() -HTTP GET /peek (list) - ↓ Integration: DirectionsConnector.DownloadDocument() -HTTP GET /dequeue (download) - ↓ -JSON Blob - ↓ Format: SimpleJson.GetBasicInfo() -E-Document Created (Imported) - ↓ Format: SimpleJson.GetCompleteInfo() -Purchase Invoice (BC) -``` - ---- - -## ⏱️ Workshop Timeline - -| Time | Duration | Activity | -|------|----------|----------| -| 00:00 | 10 min | ← You are here! (Introduction) | -| 00:10 | 30 min | **Exercise 1**: Implement SimpleJson Format | -| 00:40 | 30 min | **Exercise 2**: Implement DirectionsConnector | -| 01:10 | 15 min | Testing & Live Demo | -| 01:25 | 5 min | Wrap-up & Q&A | - ---- - -## πŸ“ Exercise 1: SimpleJson Format (30 min) - -Implement the **"E-Document" interface** - -### Part A: Check() - 5 minutes -Validate required fields before creating document -```al -procedure Check(var SourceDocumentHeader: RecordRef; ...) -begin - // TODO: Validate Customer No. - // TODO: Validate Posting Date -end; -``` - -### Part B: Create() - 15 minutes -Convert Sales Invoice to JSON -```al -procedure Create(...; var TempBlob: Codeunit "Temp Blob") -begin - // TODO: Generate JSON from Sales Invoice - // TODO: Include header and lines -end; -``` - -### Part C: GetBasicInfo() - 5 minutes -Parse incoming JSON metadata -```al -procedure GetBasicInfoFromReceivedDocument(var EDocument: Record "E-Document"; ...) -begin - // TODO: Parse JSON - // TODO: Set document type, number, date -end; -``` - -### Part D: GetCompleteInfo() - 5 minutes -Create Purchase Invoice from JSON -```al -procedure GetCompleteInfoFromReceivedDocument(...) -begin - // TODO: Parse JSON - // TODO: Create Purchase Header & Lines -end; -``` - ---- - -## πŸ”Œ Exercise 2: DirectionsConnector (30 min) - -Implement **IDocumentSender** and **IDocumentReceiver** interfaces - -### Part A: Setup - 5 minutes -Configure connection in BC -- API Base URL -- Register to get API Key -- Test connection - -### Part B: Send() - 10 minutes -Send document to API -```al -procedure Send(var EDocument: Record "E-Document"; ...) -begin - // TODO: Get JSON from SendContext - // TODO: POST to /enqueue endpoint - // TODO: Handle response -end; -``` - -### Part C: ReceiveDocuments() - 10 minutes -Get list of available documents -```al -procedure ReceiveDocuments(...; DocumentsMetadata: Codeunit "Temp Blob List"; ...) -begin - // TODO: GET from /peek endpoint - // TODO: Parse items array - // TODO: Add each to DocumentsMetadata list -end; -``` - -### Part D: DownloadDocument() - 5 minutes -Download specific document -```al -procedure DownloadDocument(var EDocument: Record "E-Document"; ...) -begin - // TODO: GET from /dequeue endpoint - // TODO: Parse response - // TODO: Store in TempBlob -end; -``` - ---- - -## 🎯 Success Criteria - -By the end, you should be able to: - -βœ… **Create** a Sales Invoice in BC -βœ… **Convert** it to JSON via SimpleJson format -βœ… **Send** it to Azure API via DirectionsConnector -βœ… **Verify** it appears in the queue -βœ… **Receive** documents from the API -βœ… **Parse** JSON and extract metadata -βœ… **Create** Purchase Invoices from received documents - -**You'll have built a complete E-Document integration!** πŸŽ‰ - ---- - -## πŸ› οΈ What's Pre-Written? - -To save time, these are already implemented: - -### SimpleJson Format: -- βœ… Extension setup (app.json) -- βœ… Enum extension -- βœ… Helper methods for JSON operations -- βœ… Error handling framework - -### DirectionsConnector: -- βœ… Connection Setup table & page -- βœ… Authentication helpers -- βœ… HTTP request builders -- βœ… Registration logic - -**You focus on the business logic!** - ---- - -## πŸ“‚ Your Workspace - -``` -application/ - β”œβ”€β”€ simple_json/ - β”‚ β”œβ”€β”€ app.json βœ… Pre-written - β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al βœ… Pre-written - β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al ⚠️ TODO sections - β”‚ └── SimpleJsonHelper.Codeunit.al βœ… Pre-written - β”‚ - └── directions_connector/ - β”œβ”€β”€ app.json βœ… Pre-written - β”œβ”€β”€ DirectionsIntegration.EnumExt.al βœ… Pre-written - β”œβ”€β”€ DirectionsIntegration.Codeunit.al ⚠️ TODO sections - β”œβ”€β”€ DirectionsConnectionSetup.Table.al βœ… Pre-written - β”œβ”€β”€ DirectionsConnectionSetup.Page.al βœ… Pre-written - β”œβ”€β”€ DirectionsAuth.Codeunit.al βœ… Pre-written - └── DirectionsRequests.Codeunit.al βœ… Pre-written -``` - ---- - -## πŸ“š Resources Available - -During the workshop: - -1. **WORKSHOP_GUIDE.md** - Step-by-step instructions with full code -2. **API_REFERENCE.md** - Complete API documentation -3. **README.md** (E-Document Core) - Framework reference -4. **Instructor** - Available for questions! - -After the workshop: -- Complete solution in `/solution/` folder -- Homework exercises -- Additional resources - ---- - -## πŸ’‘ Tips for Success - -1. **Read the TODO comments** - They contain hints and instructions -2. **Use the helper methods** - They're pre-written to save time -3. **Test incrementally** - Don't wait until the end -4. **Check the logs** - E-Document framework logs everything -5. **Ask questions** - The instructor is here to help! -6. **Have fun!** - This is a hands-on learning experience - ---- - -## πŸ› Common Pitfalls - -Watch out for: -- ❌ Missing commas in JSON -- ❌ Forgetting to add authentication headers -- ❌ Not handling empty lines/arrays -- ❌ Incorrect RecordRef table numbers -- ❌ Not logging HTTP requests/responses - -**The workshop guide has solutions for all of these!** - ---- - -## πŸŽ“ Beyond the Workshop - -After mastering the basics, explore: - -**Advanced Format Features:** -- Support multiple document types -- Add custom field mappings -- Implement validation rules -- Handle attachments (PDF, XML) - -**Advanced Integration Features:** -- Async status checking (`IDocumentResponseHandler`) -- Approval workflows (`ISentDocumentActions`) -- Batch processing -- Error handling and retry logic -- Custom actions (`IDocumentAction`) - -**Real-World Scenarios:** -- PEPPOL format -- Avalara integration -- Custom XML formats -- EDI integrations - ---- - -## 🀝 Workshop Collaboration - -### Partner Up! -- Work with a neighbor -- Share your API key to exchange documents -- Test each other's implementations - -### Group Testing -- Send documents to the group queue -- Everyone receives and processes them -- Great way to test at scale! - ---- - -## πŸš€ Let's Get Started! - -1. **Open** `WORKSHOP_GUIDE.md` for step-by-step instructions -2. **Navigate** to `application/simple_json/SimpleJsonFormat.Codeunit.al` -3. **Find** the first TODO section in the `Check()` method -4. **Start coding!** - -**Timer starts... NOW!** ⏰ - ---- - -## ❓ Questions? - -Before we start: -- ❓ Is everyone able to access the API URL? -- ❓ Does everyone have their development environment ready? -- ❓ Any questions about the architecture or flow? - ---- - -## πŸ“ž Need Help? - -During the workshop: -- πŸ™‹ Raise your hand -- πŸ’¬ Ask your neighbor -- πŸ“– Check the WORKSHOP_GUIDE.md -- πŸ” Look at the API_REFERENCE.md -- πŸ‘¨β€πŸ« Ask the instructor - -**We're all here to learn together!** - ---- - -# πŸŽ‰ Good Luck! - -**Remember**: The goal is to learn, not to finish first. -Take your time, experiment, and enjoy the process! - -**Now let's build something awesome!** πŸ’ͺ - ---- - -## Quick Links - -- πŸ“˜ [Workshop Guide](./WORKSHOP_GUIDE.md) - Step-by-step instructions -- πŸ”Œ [API Reference](./API_REFERENCE.md) - Endpoint documentation -- πŸ“ [Workshop Plan](./WORKSHOP_PLAN.md) - Overview and structure -- πŸ“š [E-Document Core README](../../../NAV_1/App/BCApps/src/Apps/W1/EDocument/App/README.md) - Framework docs - -**API Base URL**: `[Will be provided by instructor]` - ---- - -*Press `Ctrl+Shift+V` in VS Code to view this file in preview mode with formatting!* +# 🎯 E-Document Connector Workshop +## Directions EMEA 2025 + +--- + +## πŸ‘‹ Welcome! + +In the next **90 minutes**, you'll build a complete E-Document integration solution. + +**What you'll learn:** +- How the E-Document Core framework works +- How to implement format interfaces (JSON) +- How to implement integration interfaces (HTTP API) +- Complete round-trip document exchange + +**What you'll build:** +- βœ… SimpleJson Format - Convert documents to/from JSON +- βœ… DirectionsConnector - Send/receive via HTTP API + +--- + +## πŸ—οΈ Architecture Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Business Central β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Sales Invoice│───▢│ E-Document Core β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Format Interface│◀─── You implement! β”‚ +β”‚ β”‚ (SimpleJson) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ JSON Blob β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Integration Interface │◀─── You implement! β”‚ +β”‚ β”‚ (DirectionsConnector) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ HTTPS + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Azure API Server β”‚ + β”‚ β”‚ + β”‚ Queue Management β”‚ + β”‚ Document Storage β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Another Company / β”‚ + β”‚ Trading Partner β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## πŸ“¦ E-Document Core Framework + +The framework provides: + +### 1️⃣ **Document Interface** (`"E-Document"`) +Convert documents to/from Business Central + +**Outgoing** (BC β†’ External): +- `Check()` - Validate before sending +- `Create()` - Convert to format (JSON, XML, etc.) +- `CreateBatch()` - Batch multiple documents + +**Incoming** (External β†’ BC): +- `GetBasicInfoFromReceivedDocument()` - Parse metadata +- `GetCompleteInfoFromReceivedDocument()` - Create BC document + +### 2️⃣ **Integration Interfaces** +Send/receive documents via various channels + +**IDocumentSender**: +- `Send()` - Send document to external service + +**IDocumentReceiver**: +- `ReceiveDocuments()` - Get list of available documents +- `DownloadDocument()` - Download specific document + +**Others** (Advanced): +- `IDocumentResponseHandler` - Async status checking +- `ISentDocumentActions` - Approval/cancellation +- `IDocumentAction` - Custom actions + +--- + +## 🎨 What is SimpleJson? + +A simple JSON format for E-Documents designed for this workshop. + +### Example JSON Structure: + +```json +{ + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001", + "customerName": "Contoso Ltd.", + "postingDate": "2025-10-21", + "currencyCode": "USD", + "totalAmount": 1250.00, + "lines": [ + { + "lineNo": 1, + "type": "Item", + "no": "ITEM-001", + "description": "Item A", + "quantity": 5, + "unitPrice": 250.00, + "lineAmount": 1250.00 + } + ] +} +``` + +**Why JSON?** +- βœ… Human-readable +- βœ… Easy to parse +- βœ… Widely supported +- βœ… Perfect for learning + +--- + +## πŸ”Œ What is DirectionsConnector? + +An HTTP-based integration that sends/receives documents via REST API. + +### API Endpoints: + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/register` | POST | Get API key | +| `/enqueue` | POST | Send document | +| `/peek` | GET | View queue | +| `/dequeue` | GET | Receive document | +| `/clear` | DELETE | Clear queue | + +### Authentication: +```http +X-Service-Key: your-api-key-here +``` + +### Why HTTP API? +- βœ… Simple and universal +- βœ… Easy to test (browser, Postman) +- βœ… Real-world scenario +- βœ… Stateless and scalable + +--- + +## πŸ”„ Complete Flow + +### Outgoing (Sending): +``` +Sales Invoice (BC) + ↓ Post +E-Document Created + ↓ Format: SimpleJson.Create() +JSON Blob + ↓ Integration: DirectionsConnector.Send() +HTTP POST /enqueue + ↓ +Azure API Queue +``` + +### Incoming (Receiving): +``` +Azure API Queue + ↓ Integration: DirectionsConnector.ReceiveDocuments() +HTTP GET /peek (list) + ↓ Integration: DirectionsConnector.DownloadDocument() +HTTP GET /dequeue (download) + ↓ +JSON Blob + ↓ Format: SimpleJson.GetBasicInfo() +E-Document Created (Imported) + ↓ Format: SimpleJson.GetCompleteInfo() +Purchase Invoice (BC) +``` + +--- + +## ⏱️ Workshop Timeline + +| Time | Duration | Activity | +|------|----------|----------| +| 00:00 | 10 min | ← You are here! (Introduction) | +| 00:10 | 30 min | **Exercise 1**: Implement SimpleJson Format | +| 00:40 | 30 min | **Exercise 2**: Implement DirectionsConnector | +| 01:10 | 15 min | Testing & Live Demo | +| 01:25 | 5 min | Wrap-up & Q&A | + +--- + +## πŸ“ Exercise 1: SimpleJson Format (30 min) + +Implement the **"E-Document" interface** + +### Part A: Check() - 5 minutes +Validate required fields before creating document +```al +procedure Check(var SourceDocumentHeader: RecordRef; ...) +begin + // TODO: Validate Customer No. + // TODO: Validate Posting Date +end; +``` + +### Part B: Create() - 15 minutes +Convert Sales Invoice to JSON +```al +procedure Create(...; var TempBlob: Codeunit "Temp Blob") +begin + // TODO: Generate JSON from Sales Invoice + // TODO: Include header and lines +end; +``` + +### Part C: GetBasicInfo() - 5 minutes +Parse incoming JSON metadata +```al +procedure GetBasicInfoFromReceivedDocument(var EDocument: Record "E-Document"; ...) +begin + // TODO: Parse JSON + // TODO: Set document type, number, date +end; +``` + +### Part D: GetCompleteInfo() - 5 minutes +Create Purchase Invoice from JSON +```al +procedure GetCompleteInfoFromReceivedDocument(...) +begin + // TODO: Parse JSON + // TODO: Create Purchase Header & Lines +end; +``` + +--- + +## πŸ”Œ Exercise 2: DirectionsConnector (30 min) + +Implement **IDocumentSender** and **IDocumentReceiver** interfaces + +### Part A: Setup - 5 minutes +Configure connection in BC +- API Base URL +- Register to get API Key +- Test connection + +### Part B: Send() - 10 minutes +Send document to API +```al +procedure Send(var EDocument: Record "E-Document"; ...) +begin + // TODO: Get JSON from SendContext + // TODO: POST to /enqueue endpoint + // TODO: Handle response +end; +``` + +### Part C: ReceiveDocuments() - 10 minutes +Get list of available documents +```al +procedure ReceiveDocuments(...; DocumentsMetadata: Codeunit "Temp Blob List"; ...) +begin + // TODO: GET from /peek endpoint + // TODO: Parse items array + // TODO: Add each to DocumentsMetadata list +end; +``` + +### Part D: DownloadDocument() - 5 minutes +Download specific document +```al +procedure DownloadDocument(var EDocument: Record "E-Document"; ...) +begin + // TODO: GET from /dequeue endpoint + // TODO: Parse response + // TODO: Store in TempBlob +end; +``` + +--- + +## 🎯 Success Criteria + +By the end, you should be able to: + +βœ… **Create** a Sales Invoice in BC +βœ… **Convert** it to JSON via SimpleJson format +βœ… **Send** it to Azure API via DirectionsConnector +βœ… **Verify** it appears in the queue +βœ… **Receive** documents from the API +βœ… **Parse** JSON and extract metadata +βœ… **Create** Purchase Invoices from received documents + +**You'll have built a complete E-Document integration!** πŸŽ‰ + +--- + +## πŸ› οΈ What's Pre-Written? + +To save time, these are already implemented: + +### SimpleJson Format: +- βœ… Extension setup (app.json) +- βœ… Enum extension +- βœ… Helper methods for JSON operations +- βœ… Error handling framework + +### DirectionsConnector: +- βœ… Connection Setup table & page +- βœ… Authentication helpers +- βœ… HTTP request builders +- βœ… Registration logic + +**You focus on the business logic!** + +--- + +## πŸ“‚ Your Workspace + +``` +application/ + β”œβ”€β”€ simple_json/ + β”‚ β”œβ”€β”€ app.json βœ… Pre-written + β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al βœ… Pre-written + β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al ⚠️ TODO sections + β”‚ └── SimpleJsonHelper.Codeunit.al βœ… Pre-written + β”‚ + └── directions_connector/ + β”œβ”€β”€ app.json βœ… Pre-written + β”œβ”€β”€ DirectionsIntegration.EnumExt.al βœ… Pre-written + β”œβ”€β”€ DirectionsIntegration.Codeunit.al ⚠️ TODO sections + β”œβ”€β”€ DirectionsConnectionSetup.Table.al βœ… Pre-written + β”œβ”€β”€ DirectionsConnectionSetup.Page.al βœ… Pre-written + β”œβ”€β”€ DirectionsAuth.Codeunit.al βœ… Pre-written + └── DirectionsRequests.Codeunit.al βœ… Pre-written +``` + +--- + +## πŸ“š Resources Available + +During the workshop: + +1. **WORKSHOP_GUIDE.md** - Step-by-step instructions with full code +2. **API_REFERENCE.md** - Complete API documentation +3. **README.md** (E-Document Core) - Framework reference +4. **Instructor** - Available for questions! + +After the workshop: +- Complete solution in `/solution/` folder +- Homework exercises +- Additional resources + +--- + +## πŸ’‘ Tips for Success + +1. **Read the TODO comments** - They contain hints and instructions +2. **Use the helper methods** - They're pre-written to save time +3. **Test incrementally** - Don't wait until the end +4. **Check the logs** - E-Document framework logs everything +5. **Ask questions** - The instructor is here to help! +6. **Have fun!** - This is a hands-on learning experience + +--- + +## πŸ› Common Pitfalls + +Watch out for: +- ❌ Missing commas in JSON +- ❌ Forgetting to add authentication headers +- ❌ Not handling empty lines/arrays +- ❌ Incorrect RecordRef table numbers +- ❌ Not logging HTTP requests/responses + +**The workshop guide has solutions for all of these!** + +--- + +## πŸŽ“ Beyond the Workshop + +After mastering the basics, explore: + +**Advanced Format Features:** +- Support multiple document types +- Add custom field mappings +- Implement validation rules +- Handle attachments (PDF, XML) + +**Advanced Integration Features:** +- Async status checking (`IDocumentResponseHandler`) +- Approval workflows (`ISentDocumentActions`) +- Batch processing +- Error handling and retry logic +- Custom actions (`IDocumentAction`) + +**Real-World Scenarios:** +- PEPPOL format +- Avalara integration +- Custom XML formats +- EDI integrations + +--- + +## 🀝 Workshop Collaboration + +### Partner Up! +- Work with a neighbor +- Share your API key to exchange documents +- Test each other's implementations + +### Group Testing +- Send documents to the group queue +- Everyone receives and processes them +- Great way to test at scale! + +--- + +## πŸš€ Let's Get Started! + +1. **Open** `WORKSHOP_GUIDE.md` for step-by-step instructions +2. **Navigate** to `application/simple_json/SimpleJsonFormat.Codeunit.al` +3. **Find** the first TODO section in the `Check()` method +4. **Start coding!** + +**Timer starts... NOW!** ⏰ + +--- + +## ❓ Questions? + +Before we start: +- ❓ Is everyone able to access the API URL? +- ❓ Does everyone have their development environment ready? +- ❓ Any questions about the architecture or flow? + +--- + +## πŸ“ž Need Help? + +During the workshop: +- πŸ™‹ Raise your hand +- πŸ’¬ Ask your neighbor +- πŸ“– Check the WORKSHOP_GUIDE.md +- πŸ” Look at the API_REFERENCE.md +- πŸ‘¨β€πŸ« Ask the instructor + +**We're all here to learn together!** + +--- + +# πŸŽ‰ Good Luck! + +**Remember**: The goal is to learn, not to finish first. +Take your time, experiment, and enjoy the process! + +**Now let's build something awesome!** πŸ’ͺ + +--- + +## Quick Links + +- πŸ“˜ [Workshop Guide](./WORKSHOP_GUIDE.md) - Step-by-step instructions +- πŸ”Œ [API Reference](./API_REFERENCE.md) - Endpoint documentation +- πŸ“ [Workshop Plan](./WORKSHOP_PLAN.md) - Overview and structure +- πŸ“š [E-Document Core README](../../../NAV_1/App/BCApps/src/Apps/W1/EDocument/App/README.md) - Framework docs + +**API Base URL**: `[Will be provided by instructor]` + +--- + +*Press `Ctrl+Shift+V` in VS Code to view this file in preview mode with formatting!* diff --git a/samples/EDocument/DirectionsEMEA2025/WORKSHOP_PLAN.md b/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_PLAN.md similarity index 97% rename from samples/EDocument/DirectionsEMEA2025/WORKSHOP_PLAN.md rename to samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_PLAN.md index b40d428a..0f343ff0 100644 --- a/samples/EDocument/DirectionsEMEA2025/WORKSHOP_PLAN.md +++ b/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_PLAN.md @@ -1,518 +1,518 @@ -# E-Document Connector Workshop - Implementation Plan - -## Workshop Overview -**Duration**: 90 minutes -**Format**: Hands-on coding workshop -**Goal**: Build a complete E-Document solution with SimpleJson format and DirectionsConnector integration - ---- - -## Timeline - -| Time | Duration | Activity | -|------|----------|----------| -| 00:00-00:10 | 10 min | Introduction (using VS Code as presentation) | -| 00:10-00:40 | 30 min | Exercise 1 - SimpleJson Format Implementation | -| 00:40-01:10 | 30 min | Exercise 2 - DirectionsConnector Integration | -| 01:10-01:25 | 15 min | Testing & Live Demo | -| 01:25-01:30 | 5 min | Wrap-up & Q&A | - ---- - -## Architecture Overview - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Business Central β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Sales Invoice │────────▢│ E-Document Core β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ SimpleJson Format β”‚ β”‚ -β”‚ β”‚ (Exercise 1) β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ JSON Blob β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ DirectionsConnector β”‚ β”‚ -β”‚ β”‚ (Exercise 2) β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ HTTP POST - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Azure API Server β”‚ - β”‚ (Pre-deployed) β”‚ - β”‚ β”‚ - β”‚ Endpoints: β”‚ - β”‚ - POST /register β”‚ - β”‚ - POST /enqueue β”‚ - β”‚ - GET /peek β”‚ - β”‚ - GET /dequeue β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - ---- - -## What Participants Will Build - -### Exercise 1: SimpleJson Format (30 minutes) -Implement the **"E-Document" Interface** to convert Business Central documents to/from JSON format. - -The interface has two main sections: -1. **Outgoing**: Convert BC documents to E-Document blobs (`Check`, `Create`, `CreateBatch`) -2. **Incoming**: Parse E-Document blobs to BC documents (`GetBasicInfoFromReceivedDocument`, `GetCompleteInfoFromReceivedDocument`) - -**Participants will implement:** -- βœ… `Check()` method - Validate required fields before document creation (5 min) -- βœ… `Create()` method - Generate JSON from Sales Invoice (15 min) -- βœ… `GetBasicInfoFromReceivedDocument()` method - Parse incoming JSON metadata (5 min) -- βœ… `GetCompleteInfoFromReceivedDocument()` method - Create Purchase Invoice from JSON (5 min) - -**Pre-written boilerplate includes:** -- Extension setup (app.json, dependencies) -- Enum extensions -- Helper methods for JSON generation and parsing -- Error handling framework - -### Exercise 2: DirectionsConnector Integration (30 minutes) -Implement the **Integration Interface** to send and receive documents via the Azure API server. - -**Participants will implement:** -- βœ… Connection setup and registration (5 min) -- βœ… `Send()` method - POST document to /enqueue endpoint (10 min) -- βœ… `ReceiveDocuments()` method - GET documents from /peek endpoint (10 min) -- βœ… `DownloadDocument()` method - GET single document from /dequeue endpoint (5 min) - -**Pre-written boilerplate includes:** -- Setup table and page UI -- Authentication helper -- HTTP request builders -- Error logging - ---- - -## Sample JSON Format - -Participants will generate JSON in this structure: - -```json -{ - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001", - "customerName": "Contoso Ltd.", - "postingDate": "2025-10-21", - "currencyCode": "USD", - "totalAmount": 1250.00, - "lines": [ - { - "lineNo": 1, - "type": "Item", - "no": "ITEM-001", - "description": "Item A", - "quantity": 5, - "unitPrice": 250.00, - "lineAmount": 1250.00 - } - ] -} -``` - ---- - -## Azure API Server - -**Base URL**: `https://[workshop-server].azurewebsites.net` (will be provided) - -### Endpoints - -#### 1. Register User -```http -POST /register -Content-Type: application/json - -{ - "name": "participant-name" -} - -Response: -{ - "status": "ok", - "key": "uuid-api-key" -} -``` - -#### 2. Send Document (Enqueue) -```http -POST /enqueue -X-Service-Key: your-api-key -Content-Type: application/json - -{ - "documentType": "Invoice", - "documentNo": "SI-001", - ... -} - -Response: -{ - "status": "ok", - "queued_count": 1 -} -``` - -#### 3. Check Queue -```http -GET /peek -X-Service-Key: your-api-key - -Response: -{ - "queued_count": 1, - "items": [...] -} -``` - -#### 4. Retrieve Document (Dequeue) -```http -GET /dequeue -X-Service-Key: your-api-key - -Response: -{ - "document": {...} -} -``` - ---- - -## Project Structure - -``` -DirectionsEMEA2025/ -β”œβ”€β”€ WORKSHOP_INTRO.md # VS Code presentation deck -β”œβ”€β”€ WORKSHOP_GUIDE.md # Step-by-step exercises -β”œβ”€β”€ API_REFERENCE.md # Azure API documentation -β”‚ -β”œβ”€β”€ application/ -β”‚ β”œβ”€β”€ simple_json/ # Exercise 1: Format Extension -β”‚ β”‚ β”œβ”€β”€ app.json -β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al -β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al # ⚠️ TODO sections -β”‚ β”‚ └── SimpleJsonHelper.Codeunit.al # βœ… Pre-written -β”‚ β”‚ -β”‚ └── directions_connector/ # Exercise 2: Integration Extension -β”‚ β”œβ”€β”€ app.json -β”‚ β”œβ”€β”€ DirectionsIntegration.EnumExt.al -β”‚ β”œβ”€β”€ DirectionsIntegration.Codeunit.al # ⚠️ TODO sections -β”‚ β”œβ”€β”€ DirectionsSetup.Table.al # βœ… Pre-written -β”‚ β”œβ”€β”€ DirectionsSetup.Page.al # βœ… Pre-written -β”‚ β”œβ”€β”€ DirectionsAuth.Codeunit.al # βœ… Pre-written -β”‚ └── DirectionsRequests.Codeunit.al # βœ… Pre-written -β”‚ -β”œβ”€β”€ solution/ # Complete working solution -β”‚ β”œβ”€β”€ simple_json/ # Reference implementation -β”‚ └── directions_connector/ # Reference implementation -β”‚ -└── server/ - β”œβ”€β”€ server.py # FastAPI server (for reference) - β”œβ”€β”€ requirements.txt - └── README.md # Deployment info (Azure hosted) -``` - ---- - -## Deliverables - -### 1. WORKSHOP_INTRO.md -VS Code presentation covering: -- **Slide 1**: Welcome & Goals -- **Slide 2**: E-Document Framework Overview -- **Slide 3**: Architecture Diagram -- **Slide 4**: SimpleJson Format Introduction -- **Slide 5**: DirectionsConnector Overview -- **Slide 6**: Azure API Server -- **Slide 7**: Exercise Overview -- **Slide 8**: Success Criteria - -### 2. WORKSHOP_GUIDE.md -Step-by-step instructions: -- Prerequisites & setup -- **Exercise 1**: SimpleJson Format (implements "E-Document" interface) - - Part A: Implement Check() (5 min) - - Part B: Implement Create() (15 min) - - Part C: Implement GetBasicInfoFromReceivedDocument() (5 min) - - Part D: Implement GetCompleteInfoFromReceivedDocument() (5 min) - - Validation steps -- **Exercise 2**: DirectionsConnector - - Part A: Register & Setup (5 min) - - Part B: Implement Send() (10 min) - - Part C: Implement ReceiveDocuments() (10 min) - - Part D: Implement DownloadDocument() (5 min) - - Validation steps -- Testing instructions -- Troubleshooting guide - -### 3. API_REFERENCE.md -Complete API documentation: -- Base URL and authentication -- All endpoint specifications -- Request/response examples -- Error handling -- Rate limits (if any) - -### 4. SimpleJson Format Boilerplate -Files with TODO sections: -- `app.json` - Extension manifest with dependencies -- `SimpleJsonFormat.EnumExt.al` - Enum extension (pre-written) -- `SimpleJsonFormat.Codeunit.al` - Format implementation with TODOs: - ```al - // TODO: Exercise 1.A - Implement validation - procedure Check(...) - begin - // TODO: Validate Customer No. - // TODO: Validate Posting Date - // TODO: Validate at least one line exists - end; - - // TODO: Exercise 1.B - Generate JSON - procedure Create(...) - begin - // TODO: Create JSON header - // TODO: Add lines array - // TODO: Calculate totals - end; - - // TODO: Exercise 1.C - Parse incoming JSON (Basic Info) - procedure GetBasicInfoFromReceivedDocument(...) - begin - // TODO: Parse JSON from TempBlob - // TODO: Set EDocument."Document Type" - // TODO: Set EDocument."Bill-to/Pay-to No." - // TODO: Set EDocument."Bill-to/Pay-to Name" - // TODO: Set EDocument."Document Date" - // TODO: Set EDocument."Currency Code" - end; - - // TODO: Exercise 1.D - Create Purchase Invoice (Complete Info) - procedure GetCompleteInfoFromReceivedDocument(...) - begin - // TODO: Read JSON from TempBlob - // TODO: Create Purchase Header record - // TODO: Set header fields from JSON - // TODO: Create Purchase Lines from JSON array - // TODO: Return via RecordRef parameters - end; - ``` -- `SimpleJsonHelper.Codeunit.al` - Helper methods (pre-written): - - `AddJsonProperty()` - Add property to JSON - - `StartJsonObject()` - Start JSON object - - `StartJsonArray()` - Start JSON array - - `ParseJsonValue()` - Get value from JSON - - `GetJsonToken()` - Get JSON token by path - - etc. - -### 5. DirectionsConnector Boilerplate -Files with TODO sections: -- `app.json` - Extension manifest -- `DirectionsIntegration.EnumExt.al` - Enum extension (pre-written) -- `DirectionsIntegration.Codeunit.al` - Integration implementation with TODOs: - ```al - // TODO: Exercise 2.A - Setup connection - local procedure RegisterUser(...) // Provided with TODOs - - // TODO: Exercise 2.B - Send document - procedure Send(...) - begin - // TODO: Get connection setup - // TODO: Prepare HTTP request - // TODO: Set authorization header - // TODO: Send POST request - // TODO: Handle response - end; - - // TODO: Exercise 2.C - Receive documents list - procedure ReceiveDocuments(...) - begin - // TODO: Get connection setup - // TODO: Call /peek endpoint - // TODO: Parse response and create metadata blobs - end; - - // TODO: Exercise 2.D - Download single document - procedure DownloadDocument(...) - begin - // TODO: Read document ID from metadata - // TODO: Call /dequeue endpoint - // TODO: Store document content in TempBlob - end; - ``` -- Pre-written helper files: - - `DirectionsSetup.Table.al` - Connection settings (URL, API Key) - - `DirectionsSetup.Page.al` - Setup UI - - `DirectionsAuth.Codeunit.al` - Authentication helper - - `DirectionsRequests.Codeunit.al` - HTTP request builders - -### 6. Solution Folder -Complete working implementations for instructor reference: -- `/solution/simple_json/` - Fully implemented format -- `/solution/directions_connector/` - Fully implemented connector -- These are NOT given to participants initially - -### 7. Server Documentation -- `server/README.md` - Overview of the API server - - Architecture explanation - - Endpoint documentation - - Deployment notes (Azure hosted) - - No setup required (server is pre-deployed) - ---- - -## TODO Marking Convention - -In code files, use clear TODO markers with timing: - -```al -// ============================================================================ -// TODO: Exercise 1.A (10 minutes) -// Validate that required fields are filled before creating the document -// -// Instructions: -// 1. Validate that Customer No. is not empty -// 2. Validate that Posting Date is set -// 3. Validate that at least one line exists -// -// Hints: -// - Use SourceDocumentHeader.Field(FieldNo).TestField() -// - Use EDocumentErrorHelper.LogSimpleErrorMessage() for custom errors -// - Check the README.md for the Check() method example -// ============================================================================ -procedure Check(var SourceDocumentHeader: RecordRef; EDocumentService: Record "E-Document Service"; EDocumentProcessingPhase: Enum "E-Document Processing Phase") -begin - // TODO: Your code here -end; -``` - ---- - -## Success Criteria - -By the end of the workshop, participants should be able to: -- βœ… Create a sales invoice in BC -- βœ… See it converted to JSON via SimpleJson format -- βœ… Send it to Azure API server via DirectionsConnector -- βœ… Verify it appears in the queue (via /peek endpoint) -- βœ… Receive documents from the Azure API server -- βœ… Parse incoming JSON and extract metadata -- βœ… Create purchase invoices from received E-Documents -- βœ… Understand the complete E-Document round-trip flow (outgoing and incoming) -- βœ… Understand the E-Document framework architecture -- βœ… Know how to extend with additional features - ---- - -## Bonus/Homework Ideas - -For advanced participants or post-workshop: - -**Format Interface:** -- Implement `CreateBatch()` for batch processing multiple documents -- Add support for Sales Credit Memos and Purchase Credit Memos -- Add more sophisticated field mappings (dimensions, custom fields) -- Implement validation rules for incoming documents -- Support for attachments (PDF, XML) - -**Integration Interface:** -- Implement `IDocumentResponseHandler` with `GetResponse()` for async status checking -- Implement `ISentDocumentActions` for approval/cancellation workflows -- Implement `IDocumentAction` for custom actions -- Add comprehensive error handling and retry logic -- Add batch sending support - ---- - -## Notes for Implementation - -### Key Simplifications for 90-Minute Workshop -1. **Format**: Full round-trip (Create and PrepareDocument), but simplified field mapping -2. **Connector**: Full round-trip (Send and Receive), but simplified response handling -3. **Validation**: Basic field checks only -4. **Error Handling**: Use pre-written helpers -5. **UI**: Minimal - focus on code logic -6. **Document Types**: Only Sales Invoice outgoing, only Purchase Invoice incoming - -### Pre-Written vs TODO -**Participants write** (~60% of time): -- Business validation logic -- JSON structure generation -- HTTP request preparation -- Response handling - -**Pre-written boilerplate** (~40% setup time saved): -- Extension setup and dependencies -- Enum extensions -- Setup tables/pages -- Helper methods (JSON, HTTP, Auth) -- Error logging framework - -### Testing Strategy - -**Outgoing Flow (Send):** -1. Create test sales invoice with known data -2. Post and verify E-Document created -3. Check JSON blob content in E-Document log -4. Verify document sent to Azure API -5. Use /peek endpoint to confirm receipt in queue - -**Incoming Flow (Receive):** -6. Use another participant's queue or test data in API -7. Run "Get Documents" action in BC -8. Verify E-Documents appear in E-Document list with status "Imported" -9. Check downloaded JSON content in E-Document log -10. Verify basic info parsed correctly (vendor, amount, date) -11. Create Purchase Invoice from E-Document -12. Verify purchase invoice created with correct data -13. Confirm document removed from queue (via /peek) - ---- - -## Files to Create - -### Immediate (for workshop) -- [ ] `WORKSHOP_INTRO.md` - VS Code presentation -- [ ] `WORKSHOP_GUIDE.md` - Exercise instructions -- [ ] `API_REFERENCE.md` - Azure API docs -- [ ] `application/simple_json/` - Format boilerplate -- [ ] `application/directions_connector/` - Connector boilerplate - -### Reference (for instructor) -- [ ] `solution/simple_json/` - Complete format -- [ ] `solution/directions_connector/` - Complete connector -- [ ] `server/README.md` - Server documentation - -### Nice-to-have -- [ ] `TROUBLESHOOTING.md` - Common issues -- [ ] `HOMEWORK.md` - Post-workshop exercises -- [ ] Sample test data scripts - ---- - -## Next Steps - -1. Review and approve this plan -2. Get Azure server URL and deployment details -3. Create the workshop introduction (WORKSHOP_INTRO.md) -4. Create the boilerplate code with TODOs -5. Create the complete solution for reference -6. Test the full workshop flow -7. Prepare any PowerPoint backup slides (if needed) - ---- - -**Ready to implement?** Let me know if you want to adjust anything before we start building! +# E-Document Connector Workshop - Implementation Plan + +## Workshop Overview +**Duration**: 90 minutes +**Format**: Hands-on coding workshop +**Goal**: Build a complete E-Document solution with SimpleJson format and DirectionsConnector integration + +--- + +## Timeline + +| Time | Duration | Activity | +|------|----------|----------| +| 00:00-00:10 | 10 min | Introduction (using VS Code as presentation) | +| 00:10-00:40 | 30 min | Exercise 1 - SimpleJson Format Implementation | +| 00:40-01:10 | 30 min | Exercise 2 - DirectionsConnector Integration | +| 01:10-01:25 | 15 min | Testing & Live Demo | +| 01:25-01:30 | 5 min | Wrap-up & Q&A | + +--- + +## Architecture Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Business Central β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Sales Invoice │────────▢│ E-Document Core β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ SimpleJson Format β”‚ β”‚ +β”‚ β”‚ (Exercise 1) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ JSON Blob β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ DirectionsConnector β”‚ β”‚ +β”‚ β”‚ (Exercise 2) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ HTTP POST + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Azure API Server β”‚ + β”‚ (Pre-deployed) β”‚ + β”‚ β”‚ + β”‚ Endpoints: β”‚ + β”‚ - POST /register β”‚ + β”‚ - POST /enqueue β”‚ + β”‚ - GET /peek β”‚ + β”‚ - GET /dequeue β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## What Participants Will Build + +### Exercise 1: SimpleJson Format (30 minutes) +Implement the **"E-Document" Interface** to convert Business Central documents to/from JSON format. + +The interface has two main sections: +1. **Outgoing**: Convert BC documents to E-Document blobs (`Check`, `Create`, `CreateBatch`) +2. **Incoming**: Parse E-Document blobs to BC documents (`GetBasicInfoFromReceivedDocument`, `GetCompleteInfoFromReceivedDocument`) + +**Participants will implement:** +- βœ… `Check()` method - Validate required fields before document creation (5 min) +- βœ… `Create()` method - Generate JSON from Sales Invoice (15 min) +- βœ… `GetBasicInfoFromReceivedDocument()` method - Parse incoming JSON metadata (5 min) +- βœ… `GetCompleteInfoFromReceivedDocument()` method - Create Purchase Invoice from JSON (5 min) + +**Pre-written boilerplate includes:** +- Extension setup (app.json, dependencies) +- Enum extensions +- Helper methods for JSON generation and parsing +- Error handling framework + +### Exercise 2: DirectionsConnector Integration (30 minutes) +Implement the **Integration Interface** to send and receive documents via the Azure API server. + +**Participants will implement:** +- βœ… Connection setup and registration (5 min) +- βœ… `Send()` method - POST document to /enqueue endpoint (10 min) +- βœ… `ReceiveDocuments()` method - GET documents from /peek endpoint (10 min) +- βœ… `DownloadDocument()` method - GET single document from /dequeue endpoint (5 min) + +**Pre-written boilerplate includes:** +- Setup table and page UI +- Authentication helper +- HTTP request builders +- Error logging + +--- + +## Sample JSON Format + +Participants will generate JSON in this structure: + +```json +{ + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001", + "customerName": "Contoso Ltd.", + "postingDate": "2025-10-21", + "currencyCode": "USD", + "totalAmount": 1250.00, + "lines": [ + { + "lineNo": 1, + "type": "Item", + "no": "ITEM-001", + "description": "Item A", + "quantity": 5, + "unitPrice": 250.00, + "lineAmount": 1250.00 + } + ] +} +``` + +--- + +## Azure API Server + +**Base URL**: `https://[workshop-server].azurewebsites.net` (will be provided) + +### Endpoints + +#### 1. Register User +```http +POST /register +Content-Type: application/json + +{ + "name": "participant-name" +} + +Response: +{ + "status": "ok", + "key": "uuid-api-key" +} +``` + +#### 2. Send Document (Enqueue) +```http +POST /enqueue +X-Service-Key: your-api-key +Content-Type: application/json + +{ + "documentType": "Invoice", + "documentNo": "SI-001", + ... +} + +Response: +{ + "status": "ok", + "queued_count": 1 +} +``` + +#### 3. Check Queue +```http +GET /peek +X-Service-Key: your-api-key + +Response: +{ + "queued_count": 1, + "items": [...] +} +``` + +#### 4. Retrieve Document (Dequeue) +```http +GET /dequeue +X-Service-Key: your-api-key + +Response: +{ + "document": {...} +} +``` + +--- + +## Project Structure + +``` +DirectionsEMEA2025/ +β”œβ”€β”€ WORKSHOP_INTRO.md # VS Code presentation deck +β”œβ”€β”€ WORKSHOP_GUIDE.md # Step-by-step exercises +β”œβ”€β”€ API_REFERENCE.md # Azure API documentation +β”‚ +β”œβ”€β”€ application/ +β”‚ β”œβ”€β”€ simple_json/ # Exercise 1: Format Extension +β”‚ β”‚ β”œβ”€β”€ app.json +β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al +β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al # ⚠️ TODO sections +β”‚ β”‚ └── SimpleJsonHelper.Codeunit.al # βœ… Pre-written +β”‚ β”‚ +β”‚ └── directions_connector/ # Exercise 2: Integration Extension +β”‚ β”œβ”€β”€ app.json +β”‚ β”œβ”€β”€ DirectionsIntegration.EnumExt.al +β”‚ β”œβ”€β”€ DirectionsIntegration.Codeunit.al # ⚠️ TODO sections +β”‚ β”œβ”€β”€ DirectionsSetup.Table.al # βœ… Pre-written +β”‚ β”œβ”€β”€ DirectionsSetup.Page.al # βœ… Pre-written +β”‚ β”œβ”€β”€ DirectionsAuth.Codeunit.al # βœ… Pre-written +β”‚ └── DirectionsRequests.Codeunit.al # βœ… Pre-written +β”‚ +β”œβ”€β”€ solution/ # Complete working solution +β”‚ β”œβ”€β”€ simple_json/ # Reference implementation +β”‚ └── directions_connector/ # Reference implementation +β”‚ +└── server/ + β”œβ”€β”€ server.py # FastAPI server (for reference) + β”œβ”€β”€ requirements.txt + └── README.md # Deployment info (Azure hosted) +``` + +--- + +## Deliverables + +### 1. WORKSHOP_INTRO.md +VS Code presentation covering: +- **Slide 1**: Welcome & Goals +- **Slide 2**: E-Document Framework Overview +- **Slide 3**: Architecture Diagram +- **Slide 4**: SimpleJson Format Introduction +- **Slide 5**: DirectionsConnector Overview +- **Slide 6**: Azure API Server +- **Slide 7**: Exercise Overview +- **Slide 8**: Success Criteria + +### 2. WORKSHOP_GUIDE.md +Step-by-step instructions: +- Prerequisites & setup +- **Exercise 1**: SimpleJson Format (implements "E-Document" interface) + - Part A: Implement Check() (5 min) + - Part B: Implement Create() (15 min) + - Part C: Implement GetBasicInfoFromReceivedDocument() (5 min) + - Part D: Implement GetCompleteInfoFromReceivedDocument() (5 min) + - Validation steps +- **Exercise 2**: DirectionsConnector + - Part A: Register & Setup (5 min) + - Part B: Implement Send() (10 min) + - Part C: Implement ReceiveDocuments() (10 min) + - Part D: Implement DownloadDocument() (5 min) + - Validation steps +- Testing instructions +- Troubleshooting guide + +### 3. API_REFERENCE.md +Complete API documentation: +- Base URL and authentication +- All endpoint specifications +- Request/response examples +- Error handling +- Rate limits (if any) + +### 4. SimpleJson Format Boilerplate +Files with TODO sections: +- `app.json` - Extension manifest with dependencies +- `SimpleJsonFormat.EnumExt.al` - Enum extension (pre-written) +- `SimpleJsonFormat.Codeunit.al` - Format implementation with TODOs: + ```al + // TODO: Exercise 1.A - Implement validation + procedure Check(...) + begin + // TODO: Validate Customer No. + // TODO: Validate Posting Date + // TODO: Validate at least one line exists + end; + + // TODO: Exercise 1.B - Generate JSON + procedure Create(...) + begin + // TODO: Create JSON header + // TODO: Add lines array + // TODO: Calculate totals + end; + + // TODO: Exercise 1.C - Parse incoming JSON (Basic Info) + procedure GetBasicInfoFromReceivedDocument(...) + begin + // TODO: Parse JSON from TempBlob + // TODO: Set EDocument."Document Type" + // TODO: Set EDocument."Bill-to/Pay-to No." + // TODO: Set EDocument."Bill-to/Pay-to Name" + // TODO: Set EDocument."Document Date" + // TODO: Set EDocument."Currency Code" + end; + + // TODO: Exercise 1.D - Create Purchase Invoice (Complete Info) + procedure GetCompleteInfoFromReceivedDocument(...) + begin + // TODO: Read JSON from TempBlob + // TODO: Create Purchase Header record + // TODO: Set header fields from JSON + // TODO: Create Purchase Lines from JSON array + // TODO: Return via RecordRef parameters + end; + ``` +- `SimpleJsonHelper.Codeunit.al` - Helper methods (pre-written): + - `AddJsonProperty()` - Add property to JSON + - `StartJsonObject()` - Start JSON object + - `StartJsonArray()` - Start JSON array + - `ParseJsonValue()` - Get value from JSON + - `GetJsonToken()` - Get JSON token by path + - etc. + +### 5. DirectionsConnector Boilerplate +Files with TODO sections: +- `app.json` - Extension manifest +- `DirectionsIntegration.EnumExt.al` - Enum extension (pre-written) +- `DirectionsIntegration.Codeunit.al` - Integration implementation with TODOs: + ```al + // TODO: Exercise 2.A - Setup connection + local procedure RegisterUser(...) // Provided with TODOs + + // TODO: Exercise 2.B - Send document + procedure Send(...) + begin + // TODO: Get connection setup + // TODO: Prepare HTTP request + // TODO: Set authorization header + // TODO: Send POST request + // TODO: Handle response + end; + + // TODO: Exercise 2.C - Receive documents list + procedure ReceiveDocuments(...) + begin + // TODO: Get connection setup + // TODO: Call /peek endpoint + // TODO: Parse response and create metadata blobs + end; + + // TODO: Exercise 2.D - Download single document + procedure DownloadDocument(...) + begin + // TODO: Read document ID from metadata + // TODO: Call /dequeue endpoint + // TODO: Store document content in TempBlob + end; + ``` +- Pre-written helper files: + - `DirectionsSetup.Table.al` - Connection settings (URL, API Key) + - `DirectionsSetup.Page.al` - Setup UI + - `DirectionsAuth.Codeunit.al` - Authentication helper + - `DirectionsRequests.Codeunit.al` - HTTP request builders + +### 6. Solution Folder +Complete working implementations for instructor reference: +- `/solution/simple_json/` - Fully implemented format +- `/solution/directions_connector/` - Fully implemented connector +- These are NOT given to participants initially + +### 7. Server Documentation +- `server/README.md` - Overview of the API server + - Architecture explanation + - Endpoint documentation + - Deployment notes (Azure hosted) + - No setup required (server is pre-deployed) + +--- + +## TODO Marking Convention + +In code files, use clear TODO markers with timing: + +```al +// ============================================================================ +// TODO: Exercise 1.A (10 minutes) +// Validate that required fields are filled before creating the document +// +// Instructions: +// 1. Validate that Customer No. is not empty +// 2. Validate that Posting Date is set +// 3. Validate that at least one line exists +// +// Hints: +// - Use SourceDocumentHeader.Field(FieldNo).TestField() +// - Use EDocumentErrorHelper.LogSimpleErrorMessage() for custom errors +// - Check the README.md for the Check() method example +// ============================================================================ +procedure Check(var SourceDocumentHeader: RecordRef; EDocumentService: Record "E-Document Service"; EDocumentProcessingPhase: Enum "E-Document Processing Phase") +begin + // TODO: Your code here +end; +``` + +--- + +## Success Criteria + +By the end of the workshop, participants should be able to: +- βœ… Create a sales invoice in BC +- βœ… See it converted to JSON via SimpleJson format +- βœ… Send it to Azure API server via DirectionsConnector +- βœ… Verify it appears in the queue (via /peek endpoint) +- βœ… Receive documents from the Azure API server +- βœ… Parse incoming JSON and extract metadata +- βœ… Create purchase invoices from received E-Documents +- βœ… Understand the complete E-Document round-trip flow (outgoing and incoming) +- βœ… Understand the E-Document framework architecture +- βœ… Know how to extend with additional features + +--- + +## Bonus/Homework Ideas + +For advanced participants or post-workshop: + +**Format Interface:** +- Implement `CreateBatch()` for batch processing multiple documents +- Add support for Sales Credit Memos and Purchase Credit Memos +- Add more sophisticated field mappings (dimensions, custom fields) +- Implement validation rules for incoming documents +- Support for attachments (PDF, XML) + +**Integration Interface:** +- Implement `IDocumentResponseHandler` with `GetResponse()` for async status checking +- Implement `ISentDocumentActions` for approval/cancellation workflows +- Implement `IDocumentAction` for custom actions +- Add comprehensive error handling and retry logic +- Add batch sending support + +--- + +## Notes for Implementation + +### Key Simplifications for 90-Minute Workshop +1. **Format**: Full round-trip (Create and PrepareDocument), but simplified field mapping +2. **Connector**: Full round-trip (Send and Receive), but simplified response handling +3. **Validation**: Basic field checks only +4. **Error Handling**: Use pre-written helpers +5. **UI**: Minimal - focus on code logic +6. **Document Types**: Only Sales Invoice outgoing, only Purchase Invoice incoming + +### Pre-Written vs TODO +**Participants write** (~60% of time): +- Business validation logic +- JSON structure generation +- HTTP request preparation +- Response handling + +**Pre-written boilerplate** (~40% setup time saved): +- Extension setup and dependencies +- Enum extensions +- Setup tables/pages +- Helper methods (JSON, HTTP, Auth) +- Error logging framework + +### Testing Strategy + +**Outgoing Flow (Send):** +1. Create test sales invoice with known data +2. Post and verify E-Document created +3. Check JSON blob content in E-Document log +4. Verify document sent to Azure API +5. Use /peek endpoint to confirm receipt in queue + +**Incoming Flow (Receive):** +6. Use another participant's queue or test data in API +7. Run "Get Documents" action in BC +8. Verify E-Documents appear in E-Document list with status "Imported" +9. Check downloaded JSON content in E-Document log +10. Verify basic info parsed correctly (vendor, amount, date) +11. Create Purchase Invoice from E-Document +12. Verify purchase invoice created with correct data +13. Confirm document removed from queue (via /peek) + +--- + +## Files to Create + +### Immediate (for workshop) +- [ ] `WORKSHOP_INTRO.md` - VS Code presentation +- [ ] `WORKSHOP_GUIDE.md` - Exercise instructions +- [ ] `API_REFERENCE.md` - Azure API docs +- [ ] `application/simple_json/` - Format boilerplate +- [ ] `application/directions_connector/` - Connector boilerplate + +### Reference (for instructor) +- [ ] `solution/simple_json/` - Complete format +- [ ] `solution/directions_connector/` - Complete connector +- [ ] `server/README.md` - Server documentation + +### Nice-to-have +- [ ] `TROUBLESHOOTING.md` - Common issues +- [ ] `HOMEWORK.md` - Post-workshop exercises +- [ ] Sample test data scripts + +--- + +## Next Steps + +1. Review and approve this plan +2. Get Azure server URL and deployment details +3. Create the workshop introduction (WORKSHOP_INTRO.md) +4. Create the boilerplate code with TODOs +5. Create the complete solution for reference +6. Test the full workshop flow +7. Prepare any PowerPoint backup slides (if needed) + +--- + +**Ready to implement?** Let me know if you want to adjust anything before we start building! From 9d86eb2cc52344419137bc70a6df010cb3fed567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Mon, 27 Oct 2025 23:16:41 +0100 Subject: [PATCH 5/9] Minor adjustments --- .../workshop/API_REFERENCE.md | 81 --- .../workshop/COMPLETE_WORKSHOP_GUIDE.md | 565 ++++++++++++++++++ .../DirectionsEMEA2025/workshop/README.md | 357 +++++++---- 3 files changed, 800 insertions(+), 203 deletions(-) create mode 100644 samples/EDocument/DirectionsEMEA2025/workshop/COMPLETE_WORKSHOP_GUIDE.md diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/API_REFERENCE.md b/samples/EDocument/DirectionsEMEA2025/workshop/API_REFERENCE.md index 8f295658..1652af87 100644 --- a/samples/EDocument/DirectionsEMEA2025/workshop/API_REFERENCE.md +++ b/samples/EDocument/DirectionsEMEA2025/workshop/API_REFERENCE.md @@ -449,87 +449,6 @@ While you can send any valid JSON, the recommended structure for this workshop i } ``` ---- - -## Testing Tips - -### Using Browser Developer Tools - -1. Open browser developer tools (F12) -2. Go to Network tab -3. Call the API endpoints -4. Inspect request/response headers and bodies - -### Using Postman - -1. Import the endpoints into Postman -2. Set up environment variables for base URL and API key -3. Create a collection for easy testing - -### Using VS Code REST Client Extension - -Create a `.http` file: - -```http -### Variables -@baseUrl = https://workshop-server.azurewebsites.net -@apiKey = your-api-key-here - -### Register -POST {{baseUrl}}/register -Content-Type: application/json - -{ - "name": "test-user" -} - -### Enqueue Document -POST {{baseUrl}}/enqueue -X-Service-Key: {{apiKey}} -Content-Type: application/json - -{ - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001" -} - -### Peek Queue -GET {{baseUrl}}/peek -X-Service-Key: {{apiKey}} - -### Dequeue Document -GET {{baseUrl}}/dequeue -X-Service-Key: {{apiKey}} - -### Clear Queue -DELETE {{baseUrl}}/clear -X-Service-Key: {{apiKey}} -``` - ---- - -## Workshop Scenarios - -### Scenario 1: Solo Testing -1. Register with your name -2. Send documents from BC -3. Peek to verify they arrived -4. Dequeue them back into BC - -### Scenario 2: Partner Exchange -1. Each partner registers separately -2. Partner A sends documents -3. Partner B receives and processes them -4. Swap roles - -### Scenario 3: Batch Processing -1. Enqueue multiple documents -2. Peek to see the count -3. Dequeue all at once -4. Process in BC - ---- ## Support diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/COMPLETE_WORKSHOP_GUIDE.md b/samples/EDocument/DirectionsEMEA2025/workshop/COMPLETE_WORKSHOP_GUIDE.md new file mode 100644 index 00000000..8d8856f4 --- /dev/null +++ b/samples/EDocument/DirectionsEMEA2025/workshop/COMPLETE_WORKSHOP_GUIDE.md @@ -0,0 +1,565 @@ +# E-Document Connector Workshop - Complete Guide +## Directions EMEA 2025 + +Welcome! This comprehensive guide will take you through building a complete E-Document integration solution in **90 minutes**. + +--- + +## πŸ“‹ Table of Contents + +1. [Workshop Overview](#workshop-overview) +2. [Prerequisites & Setup](#prerequisites--setup) +3. [Understanding the Architecture](#understanding-the-architecture) +4. [Exercise 1: SimpleJson Format (30 min)](#exercise-1-simplejson-format-30-minutes) +5. [Exercise 2: DirectionsConnector (30 min)](#exercise-2-directionsconnector-30-minutes) +6. [Testing Complete Flow (15 min)](#testing-complete-flow-15-minutes) +7. [Troubleshooting](#troubleshooting) +8. [What's Next](#whats-next) + +--- + +## 🎯 Workshop Overview + +### What You'll Build + +By the end of this workshop, you will have created: + +1. **SimpleJson Format** - An E-Document format implementation that: + - Validates outgoing documents + - Converts Sales Invoices to JSON + - Parses incoming JSON documents + - Creates Purchase Invoices from received data + +2. **DirectionsConnector** - An HTTP API integration that: + - Sends documents to an Azure-hosted API + - Receives documents from the API queue + - Handles authentication and error responses + - Enables complete document round-trips + +### What You'll Learn + +- βœ… How the E-Document Core framework works +- βœ… How to implement the "E-Document" interface +- βœ… How to implement IDocumentSender and IDocumentReceiver interfaces +- βœ… Best practices for E-Document integrations +- βœ… Testing and debugging E-Document flows + +### Timeline + +| Time | Duration | Activity | +|------|----------|----------| +| 00:00-00:10 | 10 min | Introduction & Architecture Overview | +| 00:10-00:40 | 30 min | **Exercise 1**: SimpleJson Format | +| 00:40-01:10 | 30 min | **Exercise 2**: DirectionsConnector | +| 01:10-01:25 | 15 min | Testing & Live Demo | +| 01:25-01:30 | 5 min | Wrap-up & Q&A | + +--- + +## πŸ“¦ Prerequisites & Setup + +### Required + +- βœ… Business Central development environment (Sandbox or Docker) +- βœ… VS Code with AL Language extension +- βœ… Basic AL programming knowledge +- βœ… API Base URL (provided by instructor) + +### Workspace Structure + +Your workspace contains these folders: + +``` +application/ + β”œβ”€β”€ simple_json/ # Exercise 1: Format implementation + β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al # ⚠️ TODO sections + β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al # βœ… Pre-written + β”‚ β”œβ”€β”€ SimpleJsonHelper.Codeunit.al # βœ… Pre-written helpers + β”‚ └── app.json + β”‚ + └── directions_connector/ # Exercise 2: Integration implementation + β”œβ”€β”€ ConnectorIntegration.Codeunit.al # ⚠️ TODO sections + β”œβ”€β”€ ConnectorIntegration.EnumExt.al # βœ… Pre-written + β”œβ”€β”€ ConnectorAuth.Codeunit.al # βœ… Pre-written helpers + β”œβ”€β”€ ConnectorRequests.Codeunit.al # βœ… Pre-written helpers + β”œβ”€β”€ ConnectorConnectionSetup.Table.al # βœ… Pre-written + β”œβ”€β”€ ConnectorConnectionSetup.Page.al # βœ… Pre-written + └── app.json +``` + +### What's Pre-Written vs. What You'll Implement + +**Pre-written (to save time):** +- Extension setup and dependencies +- Enum extensions +- Helper methods for JSON and HTTP operations +- Authentication and connection setup +- UI pages and tables + +**You'll implement (core business logic):** +- Document validation +- JSON creation and parsing +- HTTP request handling +- Document sending and receiving + +--- + +## πŸ—οΈ Understanding the Architecture + +### E-Document Core Framework + +The framework provides a standardized way to integrate Business Central with external systems: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Business Central β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚Sales Invoice │─────▢│ E-Document Core β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Format Interface │◀─── Exercise 1 β”‚ +β”‚ β”‚ (SimpleJson) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ JSON Blob β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚Integration Interface│◀─── Exercise 2 β”‚ +β”‚ β”‚ (Connector) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ HTTPS + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Azure API Server β”‚ + β”‚ Queue Management β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Two Key Interfaces + +#### 1. Format Interface ("E-Document") +Converts documents between Business Central and external formats. + +**Outgoing (BC β†’ External):** +- `Check()` - Validate before sending +- `Create()` - Convert to format (JSON, XML, etc.) + +**Incoming (External β†’ BC):** +- `GetBasicInfoFromReceivedDocument()` - Parse metadata +- `GetCompleteInfoFromReceivedDocument()` - Create BC document + +#### 2. Integration Interface (IDocumentSender/IDocumentReceiver) +Handles communication with external systems. + +**Sending:** +- `Send()` - Send document to external service + +**Receiving:** +- `ReceiveDocuments()` - Get list of available documents +- `DownloadDocument()` - Download specific document + +### SimpleJson Format Structure + +```json +{ + "documentType": "Invoice", + "documentNo": "SI-001", + "customerNo": "C001", + "customerName": "Contoso Ltd.", + "postingDate": "2025-10-27", + "currencyCode": "USD", + "totalAmount": 1250.00, + "lines": [ + { + "lineNo": 10000, + "type": "Item", + "no": "ITEM-001", + "description": "Widget", + "quantity": 5, + "unitPrice": 250.00, + "lineAmount": 1250.00 + } + ] +} +``` + +### API Endpoints + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/register` | POST | Get API key | +| `/enqueue` | POST | Send document | +| `/peek` | GET | View queue | +| `/dequeue` | GET | Receive & remove document | +| `/clear` | DELETE | Clear queue | + +**Authentication:** All endpoints (except `/register`) require: +``` +X-Service-Key: your-api-key-here +``` + +--- + +## πŸš€ Exercise 1: SimpleJson Format (30 minutes) + +### Overview + +You'll implement the **"E-Document" interface** to convert Business Central documents to/from JSON format. + +**File:** `application/simple_json/SimpleJsonFormat.Codeunit.al` + +--- + +### Part A: Validate Outgoing Documents (5 minutes) + +**Goal:** Ensure required fields are filled before creating the document. + +**Find:** The `Check()` procedure (around line 27) + +**Task:** Add validation for the Posting Date field. + +**Hint:** Use the same pattern as the Customer No. validation above, using `TestField()` on the Posting Date field. + +--- + +### Part B: Create JSON from Sales Invoice (15 minutes) + +**Goal:** Generate a JSON representation of a Sales Invoice with header and lines. + +**Find:** The `CreateSalesInvoiceJson()` procedure (around line 68) + +**Task:** Complete the TODOs to add: +- customerNo and customerName to the header +- description and quantity to lines + +--- + +### Part C: Parse Incoming JSON (Basic Info) (5 minutes) + +**Goal:** Extract basic information from incoming JSON to populate E-Document fields. + +**Find:** The `GetBasicInfoFromReceivedDocument()` procedure (around line 140) + +**Task:** Complete the TODOs to extract: +- Vendor number (from "customerNo" in JSON) +- Vendor name (from "customerName" in JSON) +- Total amount (from "totalAmount" in JSON) + + +--- + +### Part D: Create Purchase Invoice from JSON (5 minutes) + +**Goal:** Create a complete Purchase Invoice record from JSON data. + +**Find:** The `GetCompleteInfoFromReceivedDocument()` procedure (around line 178) + +**Task:** Complete all the TODO sections with '???' placeholders: +- Set vendor number from JSON (from "customerNo") +- Set posting date from JSON (from "postingDate") +- Set line description from JSON (from "description") +- Set line quantity from JSON (from "quantity") +- Set line unit cost from JSON (from "unitPrice") + +--- + +### βœ… Exercise 1 Complete! + +You've now implemented the complete E-Document format interface. Build and deploy your extension before moving to Exercise 2. + +--- + +## πŸ”Œ Exercise 2: DirectionsConnector (30 minutes) + +### Overview + +You'll implement the **IDocumentSender** and **IDocumentReceiver** interfaces to send/receive documents via HTTP API. + +**File:** `application/directions_connector/ConnectorIntegration.Codeunit.al` + +--- + +### Part A: Setup Connection (5 minutes) + +Before implementing code, you need to configure the connection in Business Central. + +**Steps:** + +1. **Open Business Central** in your browser + +2. **Search** for "Connector Connection Setup" + +3. **Enter Configuration:** + - **API Base URL**: `[Provided by instructor]` + - Example: `https://edocument-workshop.azurewebsites.net/` + - **User Name**: Your unique name (e.g., "john-smith") + +4. **Click "Register"** to get your API key + - The system will call the API and save your key automatically + +5. **Click "Test Connection"** to verify + - You should see "Connection test successful!" + +**βœ… Test:** You should now have a valid API key stored in the setup. + +--- + +### Part B: Send Document (10 minutes) + +**Goal:** Send an E-Document to the API `/enqueue` endpoint. + +**Find:** The `Send()` procedure (around line 31) + +**Task:** Complete the TODOs to: +- Get the temp blob with JSON from SendContext +- Create POST request to 'enqueue' endpoint +- Send the HTTP request and handle the response + +**Hints:** +- Use `SendContext.GetTempBlob()` to get the TempBlob +- Use `ConnectorRequests.ReadJsonFromBlob()` to read JSON text from blob +- Build the endpoint URL: `ConnectorSetup."API Base URL" + 'enqueue'` +- Use `ConnectorRequests.CreatePostRequest()` to create the request +- Use `ConnectorAuth.AddAuthHeader()` to add authentication +- Use `HttpClient.Send()` to send the request +- Use `SendContext.Http().SetHttpRequestMessage()` and `SetHttpResponseMessage()` to log +- Use `ConnectorRequests.CheckResponseSuccess()` to verify success + +**βœ… Test:** This will be tested when sending a Sales Invoice as an E-Document. + +--- + +### Part C: Receive Documents List (10 minutes) + +**Goal:** Retrieve the list of available documents from the API `/peek` endpoint. + +**Find:** The `ReceiveDocuments()` procedure (around line 72) + +**Task:** Complete the TODOs to: +- Create GET request to 'peek' endpoint +- Send the HTTP request and handle the response +- Add each document TempBlob to DocumentsMetadata list + +**Hints:** +- Build endpoint URL: `ConnectorSetup."API Base URL" + 'peek'` +- Use `ConnectorRequests.CreateGetRequest()` for GET requests +- Use `ConnectorAuth.AddAuthHeader()` to add authentication +- Use `HttpClient.Send()` to send the request +- Use `ReceiveContext.Http().Set...()` to log the request/response +- Parse response: `JsonObject.ReadFrom(ResponseText)` +- Get items array: `JsonObject.Get('items', JsonToken)` then `JsonToken.AsArray()` +- For each document in array, write to TempBlob and add to `DocumentsMetadata.Add(TempBlob)` + +**βœ… Test:** This will be tested when using "Get Documents" action in BC. + +--- + +### Part D: Download Single Document (5 minutes) + +**Goal:** Download a single document from the API `/dequeue` endpoint. + +**Find:** The `DownloadDocument()` procedure (around line 135) + +**Task:** Complete the TODOs to: +- Create GET request to 'dequeue' endpoint +- Send the HTTP request and handle the response + +**Hints:** +- Build endpoint URL: `ConnectorSetup."API Base URL" + 'dequeue'` +- Use `ConnectorRequests.CreateGetRequest()` for GET requests +- Use `ConnectorAuth.AddAuthHeader()` to add authentication +- Use `HttpClient.Send()` to send the request +- Use `ReceiveContext.Http().Set...()` to log +- Parse response: `JsonObject.ReadFrom(ResponseText)` +- Get document: `JsonObject.Get('document', JsonToken)` +- Store in TempBlob: `TempBlob := ReceiveContext.GetTempBlob()` then use `ConnectorRequests.WriteTextToBlob()` + +**βœ… Test:** This will be tested in the complete flow when receiving documents. + +--- + +### βœ… Exercise 2 Complete! + +You've now implemented the complete E-Document integration interface. Build and deploy your extension before testing. + +--- + +## πŸ§ͺ Testing Complete Flow (15 minutes) + +### Setup E-Document Service + +1. **Open Business Central** + +2. **Search** for "E-Document Services" + +3. **Create New Service:** + - **Code**: `CONNECTOR` + - **Description**: `Directions Connector Workshop` + - **Document Format**: `Simple JSON Format - Exercise 1` + - **Service Integration V2**: `Connector` + +4. **Click "Setup"** to open connection setup and verify configuration + +5. **Enable the service** by toggling the "Enabled" field + +--- + +### Test Outgoing Flow (Sending Documents) + +#### Step 1: Create Sales Invoice + +1. **Search** for "Sales Invoices" +2. **Create New** invoice: + - **Customer**: Any customer (e.g., "10000") + - **Posting Date**: Today +3. **Add Lines**: + - Type: Item + - No.: Any item (e.g., "1000") + - Quantity: 5 + - Unit Price: 250 +4. **Post** the invoice + +#### Step 2: Send E-Document + +1. **Search** for "E-Documents" +2. **Find** your posted invoice (Status: "Processed") +3. **Click "Send"** action +4. **Verify** status changes to "Sent" +5. **Open the E-Document** and check: + - "E-Document Log" shows the JSON content + - "Integration Log" shows the HTTP request/response + +#### Step 3: Verify in API + +You can verify the document reached the API: + +**Option 1 - Browser with extension:** +- Install a browser extension like "ModHeader" or "Modify Header Value" +- Add header: `X-Service-Key: [your-api-key]` +- Navigate to: `[API-Base-URL]/peek` +- You should see your document in the "items" array + +**Option 2 - PowerShell:** +```powershell +$headers = @{ "X-Service-Key" = "your-api-key-here" } +Invoke-RestMethod -Uri "https://[API-URL]/peek" -Headers $headers +``` + +**βœ… Success:** You should see your Sales Invoice data in JSON format in the queue! + +--- + +### Test Incoming Flow (Receiving Documents) + +#### Step 1: Ensure Documents in Queue + +Make sure there are documents in the API queue: +- Either use documents you sent earlier +- Or have a partner send documents to your queue +- Or the instructor can provide test documents + +#### Step 2: Receive Documents + +1. **Open** "E-Document Services" +2. **Select** your CONNECTOR service +3. **Click** "Get Documents" action +4. **Wait** for processing to complete + +#### Step 3: View Received E-Documents + +1. **Search** for "E-Documents" +2. **Filter** by Status: "Imported" +3. **Open** a received E-Document +4. **Verify**: + - Document No. is populated + - Vendor No. is populated + - Vendor Name is populated + - Document Date is set + - Amount is correct +5. **Check logs**: + - "E-Document Log" shows the received JSON + - "Integration Log" shows the HTTP request/response + +#### Step 4: Create Purchase Invoice + +1. **From the E-Document**, click "Create Document" +2. **Open** the created Purchase Invoice +3. **Verify**: + - Vendor is set correctly + - Posting Date matches + - Currency matches (if specified) + - Lines are populated with: + - Item numbers + - Descriptions + - Quantities + - Unit costs + - Line amounts + +#### Step 5: Verify Queue is Empty + +Check that documents were removed from the queue: +- Use the `/peek` endpoint again +- The queue should now be empty (or have fewer documents) + +**βœ… Success:** You've completed a full document round-trip! + +--- + +## πŸ› Troubleshooting + + +### Additional Resources + +**E-Document Framework:** +- [E-Document Core Documentation](https://github.com/microsoft/BCApps/blob/main/src/Apps/W1/EDocument/App/README.md) +- [E-Document Interface Source](https://github.com/microsoft/BCApps/blob/main/src/Apps/W1/EDocument/App/src/Document/Interfaces/EDocument.Interface.al) +- [Integration Interfaces Source](https://github.com/microsoft/BCApps/tree/main/src/Apps/W1/EDocument/App/src/Integration/Interfaces) + +**Business Central:** +- [BC Developer Documentation](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/) +- [AL Language Reference](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-reference-overview) +- [AL Samples Repository](https://github.com/microsoft/AL) + +**Workshop Materials:** +- [API Reference](./API_REFERENCE.md) - Complete API documentation +- [Server README](../server/README.md) - API server details + +--- + +## πŸŽ‰ Congratulations! + +You've successfully completed the E-Document Connector Workshop! + +### What You've Accomplished + +- βœ… Implemented a complete E-Document format (SimpleJson) +- βœ… Implemented a complete E-Document integration (DirectionsConnector) +- βœ… Sent documents from Business Central to an external API +- βœ… Received documents from an external API into Business Central +- βœ… Converted between BC documents and JSON format +- βœ… Understood the E-Document framework architecture +- βœ… Gained hands-on experience with real-world integration patterns + +### Skills Gained + +- Understanding of the E-Document Core framework +- Experience implementing format interfaces +- Experience implementing integration interfaces +- HTTP API integration best practices +- JSON data mapping and transformation +- Testing and debugging E-Document flows + +--- + + +**Thank you for participating!** We hope you found this workshop valuable and are excited to build E-Document integrations in your projects. + +**Happy Coding!** πŸš€ + +--- diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/README.md b/samples/EDocument/DirectionsEMEA2025/workshop/README.md index 6f6fe29b..28a04335 100644 --- a/samples/EDocument/DirectionsEMEA2025/workshop/README.md +++ b/samples/EDocument/DirectionsEMEA2025/workshop/README.md @@ -25,36 +25,37 @@ Welcome to the E-Document Connector Workshop! This hands-on workshop teaches you ## πŸ“‚ Workshop Structure ``` -DirectionsEMEA2025/ -β”œβ”€β”€ WORKSHOP_INTRO.md # πŸ“Š VS Code presentation (10-minute intro) -β”œβ”€β”€ WORKSHOP_GUIDE.md # πŸ“˜ Step-by-step exercises with solutions -β”œβ”€β”€ API_REFERENCE.md # πŸ”Œ Complete API documentation -β”œβ”€β”€ WORKSHOP_PLAN.md # πŸ“ Detailed implementation plan +workshop/ +β”œβ”€β”€ COMPLETE_WORKSHOP_GUIDE.md # ⭐ Start here! Complete step-by-step guide +β”œβ”€β”€ API_REFERENCE.md # API endpoint documentation +β”œβ”€β”€ README.md # This file - overview and quick reference +β”œβ”€β”€ WORKSHOP_INTRO.md # Presentation slides (background material) +β”œβ”€β”€ WORKSHOP_GUIDE.md # Original exercise instructions +└── WORKSHOP_PLAN.md # Implementation plan (for reference) + +application/ +β”œβ”€β”€ simple_json/ # Exercise 1: Format implementation +β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al # ⚠️ Your code here +β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al # βœ… Pre-written +β”‚ β”œβ”€β”€ SimpleJsonHelper.Codeunit.al # βœ… Helper methods +β”‚ β”œβ”€β”€ SimpleJsonTest.Codeunit.al # βœ… Automated tests +β”‚ └── app.json β”‚ -β”œβ”€β”€ application/ -β”‚ β”œβ”€β”€ simple_json/ # Exercise 1: Format Extension -β”‚ β”‚ β”œβ”€β”€ app.json -β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al -β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al # ⚠️ TODO sections -β”‚ β”‚ └── SimpleJsonHelper.Codeunit.al # βœ… Pre-written -β”‚ β”‚ -β”‚ └── directions_connector/ # Exercise 2: Integration Extension -β”‚ β”œβ”€β”€ app.json -β”‚ β”œβ”€β”€ DirectionsIntegration.EnumExt.al -β”‚ β”œβ”€β”€ DirectionsIntegration.Codeunit.al # ⚠️ TODO sections -β”‚ β”œβ”€β”€ DirectionsConnectionSetup.Table.al # βœ… Pre-written -β”‚ β”œβ”€β”€ DirectionsConnectionSetup.Page.al # βœ… Pre-written -β”‚ β”œβ”€β”€ DirectionsAuth.Codeunit.al # βœ… Pre-written -β”‚ └── DirectionsRequests.Codeunit.al # βœ… Pre-written -β”‚ -β”œβ”€β”€ solution/ # πŸ“¦ Complete working solution (instructor) -β”‚ β”œβ”€β”€ simple_json/ -β”‚ └── directions_connector/ -β”‚ -└── server/ # 🐍 Python API server (Azure hosted) - β”œβ”€β”€ server.py - β”œβ”€β”€ requirements.txt - └── README.md +└── directions_connector/ # Exercise 2: Integration implementation + β”œβ”€β”€ ConnectorIntegration.Codeunit.al # ⚠️ Your code here + β”œβ”€β”€ ConnectorIntegration.EnumExt.al # βœ… Pre-written + β”œβ”€β”€ ConnectorAuth.Codeunit.al # βœ… Helper methods + β”œβ”€β”€ ConnectorRequests.Codeunit.al # βœ… Helper methods + β”œβ”€β”€ ConnectorConnectionSetup.Table.al # βœ… Setup table + β”œβ”€β”€ ConnectorConnectionSetup.Page.al # βœ… Setup UI + β”œβ”€β”€ ConnectorTests.Codeunit.al # βœ… Automated tests + └── app.json + +server/ +β”œβ”€β”€ app.py # API server implementation (Python/FastAPI) +β”œβ”€β”€ requirements.txt # Python dependencies +β”œβ”€β”€ startup.sh # Deployment script +└── README.md # Server documentation ``` --- @@ -63,18 +64,22 @@ DirectionsEMEA2025/ ### For Participants -1. **Read the Introduction** - Open `WORKSHOP_INTRO.md` in VS Code (Ctrl+Shift+V for preview) -2. **Follow the Guide** - Use `WORKSHOP_GUIDE.md` for step-by-step instructions -3. **Reference the API** - Check `API_REFERENCE.md` for endpoint details -4. **Ask Questions** - The instructor is here to help! +**Quick Start:** -### For Instructors +1. **Open** `COMPLETE_WORKSHOP_GUIDE.md` - This is your main guide! +2. **Ensure** you have: + - Business Central development environment + - VS Code with AL Language extension + - API Base URL from instructor +3. **Follow** the guide step-by-step through both exercises +4. **Reference** `API_REFERENCE.md` when working with HTTP endpoints -1. **Review the Plan** - See `WORKSHOP_PLAN.md` for complete overview -2. **Present the Intro** - Use `WORKSHOP_INTRO.md` as your slide deck in VS Code -3. **Provide API URL** - Update the API Base URL in materials before workshop -4. **Reference Solution** - Complete implementation in `/solution/` folder -5. **Monitor Progress** - Check `/peek` endpoint to see participant submissions +**Workshop Timeline:** +- 00:00-00:10: Introduction & Architecture (optional: review `WORKSHOP_INTRO.md`) +- 00:10-00:40: Exercise 1 - SimpleJson Format +- 00:40-01:10: Exercise 2 - DirectionsConnector +- 01:10-01:25: Testing & Live Demo +- 01:25-01:30: Wrap-up & Q&A --- @@ -123,15 +128,13 @@ By the end of this workshop, participants will be able to: ### E-Document Format Interface Converts Business Central documents to/from external formats: -- **Outgoing**: `Check()`, `Create()`, `CreateBatch()` +- **Outgoing**: `Check()`, `Create()` - **Incoming**: `GetBasicInfoFromReceivedDocument()`, `GetCompleteInfoFromReceivedDocument()` ### Integration Interfaces Communicates with external systems: - **IDocumentSender**: `Send()` - Send documents - **IDocumentReceiver**: `ReceiveDocuments()`, `DownloadDocument()` - Receive documents -- **IDocumentResponseHandler**: `GetResponse()` - Async status (advanced) -- **ISentDocumentActions**: Approval/cancellation workflows (advanced) ### SimpleJson Format - Simple JSON structure for learning @@ -139,7 +142,7 @@ Communicates with external systems: - Lines array: line items with quantities and prices - Human-readable and easy to debug -### DirectionsConnector +### Connector - REST API integration via HTTP - Authentication via API key header - Queue-based document exchange @@ -147,39 +150,6 @@ Communicates with external systems: --- -## πŸ§ͺ Testing Scenarios - -### Solo Testing -1. Send documents from your BC instance -2. Verify in API queue via `/peek` -3. Receive documents back into BC -4. Create purchase invoices - -### Partner Testing -1. Partner A sends documents -2. Partner B receives and processes -3. Swap roles and repeat -4. Great for testing interoperability! - -### Group Testing -- Multiple participants send to same queue -- Everyone receives mixed documents -- Tests error handling and validation - ---- - -## πŸ› Troubleshooting - -See the **Troubleshooting** section in `WORKSHOP_GUIDE.md` for: -- Connection issues -- Authentication problems -- JSON parsing errors -- Document creation failures - -Common solutions provided for all scenarios! - ---- - ## πŸ“– Additional Resources ### Workshop Materials @@ -189,86 +159,229 @@ Common solutions provided for all scenarios! - [Workshop Plan](WORKSHOP_PLAN.md) - Implementation overview ### E-Document Framework -- [E-Document Core README](../../NAV_1/App/BCApps/src/Apps/W1/EDocument/App/README.md) - Official framework documentation -- [E-Document Interface](../../NAV_1/App/BCApps/src/Apps/W1/EDocument/App/src/Document/Interfaces/EDocument.Interface.al) - Interface source code +- [E-Document Core README](https://github.com/microsoft/BCApps/blob/main/src/Apps/W1/EDocument/App/README.md) - Official framework documentation +- [E-Document Interface](https://github.com/microsoft/BCApps/blob/main/src/Apps/W1/EDocument/App/src/Document/Interfaces/EDocument.Interface.al) - Interface source code ### External Resources - [Business Central Documentation](https://learn.microsoft.com/dynamics365/business-central/) - [AL Language Reference](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-reference-overview) -- [FastAPI Documentation](https://fastapi.tiangolo.com/) - For the server implementation + + +## 🎯 Quick Start + +### 5-Minute Setup + +1. **Get API Access:** + - Instructor provides API Base URL: `https://[server].azurewebsites.net/` + - Open Business Central + - Search "Connector Connection Setup" + - Enter API Base URL and your unique name + - Click "Register" to get API key + +2. **Create E-Document Service:** + - Search "E-Document Services" + - Create new service with: + - Code: `CONNECTOR` + - Document Format: `Simple JSON Format - Exercise 1` + - Service Integration V2: `Connector` + - Enable the service + +3. **Start Coding:** + - Open `application/simple_json/SimpleJsonFormat.Codeunit.al` + - Find first TODO comment + - Follow `COMPLETE_WORKSHOP_GUIDE.md` + +### Quick Test + +**Outgoing (Send):** +``` +1. Post a Sales Invoice +2. Open E-Documents β†’ Find your invoice +3. Click "Send" +4. Check status changes to "Sent" +``` + +**Incoming (Receive):** +``` +1. E-Document Services β†’ Select CONNECTOR +2. Click "Get Documents" +3. E-Documents β†’ Filter Status: "Imported" +4. Click "Create Document" β†’ Opens Purchase Invoice +``` + +--- + +## πŸ“– Documentation Quick Reference + +| Document | Purpose | When to Use | +|----------|---------|-------------| +| [**COMPLETE_WORKSHOP_GUIDE.md**](./COMPLETE_WORKSHOP_GUIDE.md) | ⭐ Main guide with all exercises and solutions | Start here - your primary reference | +| [**API_REFERENCE.md**](./API_REFERENCE.md) | Complete API endpoint documentation | When implementing HTTP calls | +| [**WORKSHOP_INTRO.md**](./WORKSHOP_INTRO.md) | Presentation slides (architecture overview) | For understanding concepts | +| [**WORKSHOP_GUIDE.md**](./WORKSHOP_GUIDE.md) | Original exercise instructions | Alternative detailed guide | +| [**WORKSHOP_PLAN.md**](./WORKSHOP_PLAN.md) | Implementation plan and structure | For instructors/reference | + +--- + +## πŸ† Success Criteria + +You've completed the workshop successfully if you can: + +- βœ… Post a Sales Invoice and see it as an E-Document +- βœ… View the JSON content in the E-Document log +- βœ… Send the E-Document to the API +- βœ… Verify the document appears in the API queue (using `/peek`) +- βœ… Receive documents from the API queue +- βœ… Create Purchase Invoices from received E-Documents +- βœ… Verify all data is mapped correctly (vendor, dates, lines, amounts) +- βœ… Understand the complete round-trip flow --- -## 🎁 Bonus Content +## πŸ”§ Code Locations + +### Exercise 1: SimpleJson Format -After completing the workshop, try these challenges: +**File:** `application/simple_json/SimpleJsonFormat.Codeunit.al` -### Format Enhancements -- Add support for Credit Memos -- Implement batch processing (`CreateBatch()`) -- Add custom field mappings -- Support attachments (PDF, XML) +**TODOs:** +- Line ~30: `Check()` - Add Posting Date validation +- Line ~93: `CreateSalesInvoiceJson()` - Add customer fields to header +- Line ~110: `CreateSalesInvoiceJson()` - Add description and quantity to lines +- Line ~165: `GetBasicInfoFromReceivedDocument()` - Parse vendor info and amount +- Line ~195: `GetCompleteInfoFromReceivedDocument()` - Set vendor and dates +- Line ~215: `GetCompleteInfoFromReceivedDocument()` - Set line details -### Integration Enhancements -- Implement `IDocumentResponseHandler` for async status -- Add retry logic and error handling -- Implement `ISentDocumentActions` for approvals -- Add custom actions with `IDocumentAction` +### Exercise 2: DirectionsConnector -### Real-World Applications -- Connect to actual PEPPOL networks -- Integrate with Avalara or other tax services -- Build EDI integrations -- Create custom XML formats for local requirements +**File:** `application/directions_connector/ConnectorIntegration.Codeunit.al` + +**TODOs:** +- Line ~45: `Send()` - Get TempBlob and read JSON +- Line ~50: `Send()` - Create POST request to enqueue +- Line ~55: `Send()` - Send HTTP request +- Line ~85: `ReceiveDocuments()` - Create GET request to peek +- Line ~90: `ReceiveDocuments()` - Send HTTP request +- Line ~105: `ReceiveDocuments()` - Add documents to metadata list +- Line ~120: `DownloadDocument()` - Create GET request to dequeue +- Line ~125: `DownloadDocument()` - Send HTTP request --- -## 🀝 Contributing +## πŸ§ͺ Testing Your Implementation + +### Automated Tests + +Run the built-in tests to verify your implementation: + +1. Open **Test Tool** in Business Central +2. Select Codeunit **50113 "SimpleJson Test"** +3. Run all tests - should pass: + - `TestExercise1_CheckValidation` + - `TestExercise1_CheckCreate` + - `TestExercise2_OutgoingMethodsWork` + - `TestExercise2_GetBasicInfoFromJSON` + - `TestExercise2_CreatePurchaseInvoiceFromJSON` + +### Manual Testing -This workshop is part of the BCTech repository. If you have suggestions or improvements: +**Test Outgoing:** +```powershell +# After sending from BC, check the queue: +$headers = @{ "X-Service-Key" = "your-api-key" } +Invoke-RestMethod -Uri "https://[API-URL]/peek" -Headers $headers +``` -1. Open an issue on GitHub -2. Submit a pull request -3. Share your feedback with the instructor +**Test Incoming:** +```powershell +# Check how many documents are waiting: +$headers = @{ "X-Service-Key" = "your-api-key" } +$response = Invoke-RestMethod -Uri "https://[API-URL]/peek" -Headers $headers +Write-Host "Documents in queue: $($response.queued_count)" +``` --- -## πŸ“„ License +## πŸ› Common Issues & Solutions -This workshop material is provided under the MIT License. See the repository root for details. +| Issue | Solution | +|-------|----------| +| "Unauthorized or invalid key" | Re-register in Connector Connection Setup | +| "Failed to parse JSON" | Check JSON structure in E-Document log, verify all required fields | +| "Vendor does not exist" | Create vendor with matching number, or modify JSON to use existing vendor | +| "Document type not supported" | Verify "Simple JSON Format" is selected in E-Document Service | +| "Queue empty" | Send documents first, or ask partner/instructor for test data | +| "Connection failed" | Check API Base URL (must end with `/`), verify server is running | --- -## πŸ™ Acknowledgments +## πŸ’‘ Tips for Success -- **Microsoft Business Central Team** - For the E-Document Core framework -- **BCTech Community** - For continuous contributions -- **Workshop Participants** - For your enthusiasm and feedback! +1. **Read TODO comments carefully** - They contain important hints +2. **Use the helper methods** - Pre-written functions save time +3. **Test incrementally** - Don't wait until everything is done +4. **Check logs** - E-Document logs show JSON and HTTP details +5. **Use API_REFERENCE.md** - Complete endpoint documentation +6. **Ask questions** - Instructor is here to help! +7. **Collaborate** - Exchange documents with other participants --- -## πŸ“ž Support +## πŸ“š Learning Resources -### During the Workshop -- Ask the instructor -- Check the WORKSHOP_GUIDE.md -- Collaborate with neighbors +### E-Document Framework +- **Core README**: [E-Document Core Documentation](https://github.com/microsoft/BCApps/blob/main/src/Apps/W1/EDocument/App/README.md) +- **Interface Source**: [E-Document Interface Code](https://github.com/microsoft/BCApps/blob/main/src/Apps/W1/EDocument/App/src/Document/Interfaces/EDocument.Interface.al) +- **Integration Interfaces**: [Integration Interface Code](https://github.com/microsoft/BCApps/tree/main/src/Apps/W1/EDocument/App/src/Integration/Interfaces) -### After the Workshop -- GitHub Issues: [BCTech Repository](https://github.com/microsoft/BCTech) -- Documentation: [Learn Microsoft](https://learn.microsoft.com/dynamics365/business-central/) -- Community: [Business Central Forums](https://community.dynamics.com/forums/thread/) +### Business Central Development +- [BC Developer Documentation](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/) +- [AL Language Reference](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-reference-overview) +- [AL Samples Repository](https://github.com/microsoft/AL) + +### Workshop Materials +- **This Repository**: [BCTech E-Document Samples](https://github.com/microsoft/BCTech/tree/main/samples/EDocument) +- **API Server Code**: `server/app.py` (Python FastAPI implementation) --- -## 🎯 Quick Start +## πŸŽ“ Advanced Topics (Post-Workshop) + +After completing the basics, explore: + +**Format Enhancements:** +- Support for Credit Memos +- Support for Purchase Orders +- Custom field mappings +- Validation rules +- Document attachments + +**Integration Enhancements:** +- Batch operations +- Async status checking (`IDocumentResponseHandler`) +- Approval workflows (`ISentDocumentActions`) +- Custom actions (`IDocumentAction`) +- Error handling and retry logic + +**Real-World Examples:** +- PEPPOL format implementation +- Avalara connector integration +- Custom XML formats +- EDI integrations + +--- + +## 🀝 Workshop Collaboration -**Ready to begin? Follow these steps:** +**Share with Others:** +- Exchange your API key with a partner +- Send documents to each other's queues +- Test receiving from different sources -1. βœ… Open `WORKSHOP_INTRO.md` and read through the introduction -2. βœ… Get the API Base URL from your instructor -3. βœ… Open `WORKSHOP_GUIDE.md` and start Exercise 1 -4. βœ… Have fun building your E-Document integration! +**Group Activities:** +- Create a shared test queue +- Send documents to the group +- Practice handling various document formats --- From 80fd6cfe982d905cb615c5c8c5baa530342faa35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Mon, 27 Oct 2025 23:29:44 +0100 Subject: [PATCH 6/9] Minor adjustments --- .../workshop/COMPLETE_WORKSHOP_GUIDE.md | 1 - .../DirectionsEMEA2025/workshop/README.md | 114 +--- .../workshop/WORKSHOP_GUIDE.md | 601 ------------------ .../workshop/WORKSHOP_INTRO.md | 487 -------------- .../workshop/WORKSHOP_PLAN.md | 518 --------------- 5 files changed, 9 insertions(+), 1712 deletions(-) delete mode 100644 samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_GUIDE.md delete mode 100644 samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_INTRO.md delete mode 100644 samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_PLAN.md diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/COMPLETE_WORKSHOP_GUIDE.md b/samples/EDocument/DirectionsEMEA2025/workshop/COMPLETE_WORKSHOP_GUIDE.md index 8d8856f4..8d58babf 100644 --- a/samples/EDocument/DirectionsEMEA2025/workshop/COMPLETE_WORKSHOP_GUIDE.md +++ b/samples/EDocument/DirectionsEMEA2025/workshop/COMPLETE_WORKSHOP_GUIDE.md @@ -557,7 +557,6 @@ You've successfully completed the E-Document Connector Workshop! --- - **Thank you for participating!** We hope you found this workshop valuable and are excited to build E-Document integrations in your projects. **Happy Coding!** πŸš€ diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/README.md b/samples/EDocument/DirectionsEMEA2025/workshop/README.md index 28a04335..3efe07ec 100644 --- a/samples/EDocument/DirectionsEMEA2025/workshop/README.md +++ b/samples/EDocument/DirectionsEMEA2025/workshop/README.md @@ -29,9 +29,6 @@ workshop/ β”œβ”€β”€ COMPLETE_WORKSHOP_GUIDE.md # ⭐ Start here! Complete step-by-step guide β”œβ”€β”€ API_REFERENCE.md # API endpoint documentation β”œβ”€β”€ README.md # This file - overview and quick reference -β”œβ”€β”€ WORKSHOP_INTRO.md # Presentation slides (background material) -β”œβ”€β”€ WORKSHOP_GUIDE.md # Original exercise instructions -└── WORKSHOP_PLAN.md # Implementation plan (for reference) application/ β”œβ”€β”€ simple_json/ # Exercise 1: Format implementation @@ -51,11 +48,6 @@ application/ β”œβ”€β”€ ConnectorTests.Codeunit.al # βœ… Automated tests └── app.json -server/ -β”œβ”€β”€ app.py # API server implementation (Python/FastAPI) -β”œβ”€β”€ requirements.txt # Python dependencies -β”œβ”€β”€ startup.sh # Deployment script -└── README.md # Server documentation ``` --- @@ -91,7 +83,7 @@ server/ | 00:10-00:40 | 30 min | Exercise 1: SimpleJson Format | WORKSHOP_GUIDE.md | | 00:40-01:10 | 30 min | Exercise 2: DirectionsConnector | WORKSHOP_GUIDE.md | | 01:10-01:25 | 15 min | Testing & Live Demo | WORKSHOP_GUIDE.md | -| 01:25-01:30 | 5 min | Wrap-up & Q&A | - | +| 01:25-01:30 | 5 min | Wrap-up | - | --- @@ -186,6 +178,12 @@ Communicates with external systems: - Service Integration V2: `Connector` - Enable the service +2. **Create E-Document Workflow:** + - Create Workflow with E-Document + - When E-document Created -> Send E-Document using setup: `CONNECTOR` + - Enable workflow, and assign it in document sending profile + - Assign document sending profile to Customer. + 3. **Start Coding:** - Open `application/simple_json/SimpleJsonFormat.Codeunit.al` - Find first TODO comment @@ -204,9 +202,8 @@ Communicates with external systems: **Incoming (Receive):** ``` 1. E-Document Services β†’ Select CONNECTOR -2. Click "Get Documents" -3. E-Documents β†’ Filter Status: "Imported" -4. Click "Create Document" β†’ Opens Purchase Invoice +2. Click "Receive" +3. New E-Documents arrive ``` --- @@ -217,9 +214,6 @@ Communicates with external systems: |----------|---------|-------------| | [**COMPLETE_WORKSHOP_GUIDE.md**](./COMPLETE_WORKSHOP_GUIDE.md) | ⭐ Main guide with all exercises and solutions | Start here - your primary reference | | [**API_REFERENCE.md**](./API_REFERENCE.md) | Complete API endpoint documentation | When implementing HTTP calls | -| [**WORKSHOP_INTRO.md**](./WORKSHOP_INTRO.md) | Presentation slides (architecture overview) | For understanding concepts | -| [**WORKSHOP_GUIDE.md**](./WORKSHOP_GUIDE.md) | Original exercise instructions | Alternative detailed guide | -| [**WORKSHOP_PLAN.md**](./WORKSHOP_PLAN.md) | Implementation plan and structure | For instructors/reference | --- @@ -268,52 +262,6 @@ You've completed the workshop successfully if you can: --- -## πŸ§ͺ Testing Your Implementation - -### Automated Tests - -Run the built-in tests to verify your implementation: - -1. Open **Test Tool** in Business Central -2. Select Codeunit **50113 "SimpleJson Test"** -3. Run all tests - should pass: - - `TestExercise1_CheckValidation` - - `TestExercise1_CheckCreate` - - `TestExercise2_OutgoingMethodsWork` - - `TestExercise2_GetBasicInfoFromJSON` - - `TestExercise2_CreatePurchaseInvoiceFromJSON` - -### Manual Testing - -**Test Outgoing:** -```powershell -# After sending from BC, check the queue: -$headers = @{ "X-Service-Key" = "your-api-key" } -Invoke-RestMethod -Uri "https://[API-URL]/peek" -Headers $headers -``` - -**Test Incoming:** -```powershell -# Check how many documents are waiting: -$headers = @{ "X-Service-Key" = "your-api-key" } -$response = Invoke-RestMethod -Uri "https://[API-URL]/peek" -Headers $headers -Write-Host "Documents in queue: $($response.queued_count)" -``` - ---- - -## πŸ› Common Issues & Solutions - -| Issue | Solution | -|-------|----------| -| "Unauthorized or invalid key" | Re-register in Connector Connection Setup | -| "Failed to parse JSON" | Check JSON structure in E-Document log, verify all required fields | -| "Vendor does not exist" | Create vendor with matching number, or modify JSON to use existing vendor | -| "Document type not supported" | Verify "Simple JSON Format" is selected in E-Document Service | -| "Queue empty" | Send documents first, or ask partner/instructor for test data | -| "Connection failed" | Check API Base URL (must end with `/`), verify server is running | - ---- ## πŸ’‘ Tips for Success @@ -339,50 +287,6 @@ Write-Host "Documents in queue: $($response.queued_count)" - [AL Language Reference](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-reference-overview) - [AL Samples Repository](https://github.com/microsoft/AL) -### Workshop Materials -- **This Repository**: [BCTech E-Document Samples](https://github.com/microsoft/BCTech/tree/main/samples/EDocument) -- **API Server Code**: `server/app.py` (Python FastAPI implementation) - ---- - -## πŸŽ“ Advanced Topics (Post-Workshop) - -After completing the basics, explore: - -**Format Enhancements:** -- Support for Credit Memos -- Support for Purchase Orders -- Custom field mappings -- Validation rules -- Document attachments - -**Integration Enhancements:** -- Batch operations -- Async status checking (`IDocumentResponseHandler`) -- Approval workflows (`ISentDocumentActions`) -- Custom actions (`IDocumentAction`) -- Error handling and retry logic - -**Real-World Examples:** -- PEPPOL format implementation -- Avalara connector integration -- Custom XML formats -- EDI integrations - ---- - -## 🀝 Workshop Collaboration - -**Share with Others:** -- Exchange your API key with a partner -- Send documents to each other's queues -- Test receiving from different sources - -**Group Activities:** -- Create a shared test queue -- Send documents to the group -- Practice handling various document formats - --- **Happy Coding!** πŸš€ diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_GUIDE.md b/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_GUIDE.md deleted file mode 100644 index 48741b56..00000000 --- a/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_GUIDE.md +++ /dev/null @@ -1,601 +0,0 @@ -# Directions EMEA 2025 - E-Document Connector Workshop - -## Workshop Guide - -Welcome to the E-Document Connector Workshop! In this hands-on session, you'll build a complete E-Document solution that integrates Business Central with an external API using the E-Document Core framework. - ---- - -## 🎯 What You'll Build - -By the end of this workshop, you will have: -1. **SimpleJson Format** - Convert Sales Invoices to JSON and parse incoming Purchase Invoices -2. **DirectionsConnector** - Send and receive documents via HTTP API -3. **Complete Integration** - Full round-trip document exchange - ---- - -## ⏱️ Timeline - -- **Exercise 1** (30 min): Implement SimpleJson Format -- **Exercise 2** (30 min): Implement DirectionsConnector -- **Testing** (15 min): End-to-end validation - ---- - -## πŸ“‹ Prerequisites - -### Required -- Business Central environment (Sandbox or Docker) -- VS Code with AL Language extension -- API Base URL: `[Provided by instructor]` - -### Workshop Files -Your workspace contains: -``` -application/ - β”œβ”€β”€ simple_json/ # Exercise 1 - └── directions_connector/ # Exercise 2 -``` - ---- - -## πŸš€ Exercise 1: SimpleJson Format (30 minutes) - -In this exercise, you'll implement the **"E-Document" interface** to convert Business Central documents to/from JSON format. - -### Part A: Validate Outgoing Documents (5 minutes) - -**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` - -**Find**: The `Check()` procedure (around line 27) - -**Task**: Add validation to ensure required fields are filled before creating the document. - -**Implementation**: -```al -procedure Check(var SourceDocumentHeader: RecordRef; EDocumentService: Record "E-Document Service"; EDocumentProcessingPhase: Enum "E-Document Processing Phase") -var - SalesInvoiceHeader: Record "Sales Invoice Header"; -begin - case SourceDocumentHeader.Number of - Database::"Sales Invoice Header": - begin - // Validate Customer No. is filled - SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Sell-to Customer No.")).TestField(); - - // Validate Posting Date is filled - SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Posting Date")).TestField(); - end; - end; -end; -``` - -**βœ… Validation**: Try posting a Sales Invoice - it should validate required fields. - ---- - -### Part B: Create JSON from Sales Invoice (15 minutes) - -**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` - -**Find**: The `CreateSalesInvoiceJson()` procedure (around line 93) - -**Task**: Generate JSON representation of a Sales Invoice with header and lines. - -**Implementation**: -```al -local procedure CreateSalesInvoiceJson(var SourceDocumentHeader: RecordRef; var SourceDocumentLines: RecordRef; var OutStr: OutStream) -var - SalesInvoiceHeader: Record "Sales Invoice Header"; - SalesInvoiceLine: Record "Sales Invoice Line"; - RootObject: JsonObject; - LinesArray: JsonArray; - LineObject: JsonObject; - JsonText: Text; -begin - // Get the actual records from RecordRef - SourceDocumentHeader.SetTable(SalesInvoiceHeader); - SourceDocumentLines.SetTable(SalesInvoiceLine); - - // Add header fields to JSON object - RootObject.Add('documentType', 'Invoice'); - RootObject.Add('documentNo', SalesInvoiceHeader."No."); - RootObject.Add('customerNo', SalesInvoiceHeader."Sell-to Customer No."); - RootObject.Add('customerName', SalesInvoiceHeader."Sell-to Customer Name"); - RootObject.Add('postingDate', Format(SalesInvoiceHeader."Posting Date", 0, '--')); - RootObject.Add('currencyCode', SalesInvoiceHeader."Currency Code"); - RootObject.Add('totalAmount', SalesInvoiceHeader."Amount Including VAT"); - - // Create lines array - if SalesInvoiceLine.FindSet() then - repeat - // Create line object - Clear(LineObject); - LineObject.Add('lineNo', SalesInvoiceLine."Line No."); - LineObject.Add('type', Format(SalesInvoiceLine.Type)); - LineObject.Add('no', SalesInvoiceLine."No."); - LineObject.Add('description', SalesInvoiceLine.Description); - LineObject.Add('quantity', SalesInvoiceLine.Quantity); - LineObject.Add('unitPrice', SalesInvoiceLine."Unit Price"); - LineObject.Add('lineAmount', SalesInvoiceLine."Amount Including VAT"); - LinesArray.Add(LineObject); - until SalesInvoiceLine.Next() = 0; - - // Add lines array to root object - RootObject.Add('lines', LinesArray); - - // Write JSON to stream - RootObject.WriteTo(JsonText); - OutStr.WriteText(JsonText); -end; -``` - -**βœ… Validation**: -1. Create and post a Sales Invoice -2. Open the E-Document list -3. View the E-Document and check the JSON content in the log - ---- - -### Part C: Parse Incoming JSON (Basic Info) (5 minutes) - -**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` - -**Find**: The `GetBasicInfoFromReceivedDocument()` procedure (around line 151) - -**Task**: Extract basic information from incoming JSON to populate E-Document fields. - -**Implementation**: -```al -procedure GetBasicInfoFromReceivedDocument(var EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob") -var - JsonObject: JsonObject; - JsonToken: JsonToken; - SimpleJsonHelper: Codeunit "SimpleJson Helper"; -begin - // Parse JSON from blob - if not SimpleJsonHelper.ReadJsonFromBlob(TempBlob, JsonObject) then - Error('Failed to parse JSON document'); - - // Set document type to Purchase Invoice - EDocument."Document Type" := EDocument."Document Type"::"Purchase Invoice"; - - // Extract document number - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'documentNo', JsonToken) then - EDocument."Document No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Document No.")); - - // Extract vendor number (from customerNo in JSON) - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerNo', JsonToken) then - EDocument."Bill-to/Pay-to No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Bill-to/Pay-to No.")); - - // Extract vendor name - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerName', JsonToken) then - EDocument."Bill-to/Pay-to Name" := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Bill-to/Pay-to Name")); - - // Extract posting date - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then - EDocument."Document Date" := SimpleJsonHelper.GetJsonTokenDate(JsonToken); - - // Extract currency code - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'currencyCode', JsonToken) then - EDocument."Currency Code" := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Currency Code")); -end; -``` - -**βœ… Validation**: This will be tested in Exercise 2 when receiving documents. - ---- - -### Part D: Create Purchase Invoice from JSON (5 minutes) - -**File**: `application/simple_json/SimpleJsonFormat.Codeunit.al` - -**Find**: The `GetCompleteInfoFromReceivedDocument()` procedure (around line 188) - -**Task**: Create a Purchase Invoice record from JSON data. - -**Implementation**: -```al -procedure GetCompleteInfoFromReceivedDocument(var EDocument: Record "E-Document"; var CreatedDocumentHeader: RecordRef; var CreatedDocumentLines: RecordRef; var TempBlob: Codeunit "Temp Blob") -var - PurchaseHeader: Record "Purchase Header"; - PurchaseLine: Record "Purchase Line"; - JsonObject: JsonObject; - JsonToken: JsonToken; - JsonArray: JsonArray; - JsonLineToken: JsonToken; - SimpleJsonHelper: Codeunit "SimpleJson Helper"; - LineNo: Integer; -begin - // Parse JSON from blob - if not SimpleJsonHelper.ReadJsonFromBlob(TempBlob, JsonObject) then - Error('Failed to parse JSON document'); - - // Create Purchase Header - PurchaseHeader.Init(); - PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::Invoice; - PurchaseHeader.Insert(true); - - // Set vendor from JSON - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'customerNo', JsonToken) then - PurchaseHeader.Validate("Buy-from Vendor No.", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); - - // Set posting date - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then - PurchaseHeader.Validate("Posting Date", SimpleJsonHelper.GetJsonTokenDate(JsonToken)); - - // Set currency code (if not blank) - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'currencyCode', JsonToken) then begin - if SimpleJsonHelper.GetJsonTokenValue(JsonToken) <> '' then - PurchaseHeader.Validate("Currency Code", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); - end; - - PurchaseHeader.Modify(true); - - // Create Purchase Lines from JSON array - if JsonObject.Get('lines', JsonToken) then begin - JsonArray := JsonToken.AsArray(); - LineNo := 10000; - - foreach JsonLineToken in JsonArray do begin - JsonObject := JsonLineToken.AsObject(); - - PurchaseLine.Init(); - PurchaseLine."Document Type" := PurchaseHeader."Document Type"; - PurchaseLine."Document No." := PurchaseHeader."No."; - PurchaseLine."Line No." := LineNo; - - // Set type (default to Item) - PurchaseLine.Type := PurchaseLine.Type::Item; - - // Set item number - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'no', JsonToken) then - PurchaseLine.Validate("No.", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); - - // Set description - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'description', JsonToken) then - PurchaseLine.Description := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(PurchaseLine.Description)); - - // Set quantity - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'quantity', JsonToken) then - PurchaseLine.Validate(Quantity, SimpleJsonHelper.GetJsonTokenDecimal(JsonToken)); - - // Set unit price - if SimpleJsonHelper.SelectJsonToken(JsonObject, 'unitPrice', JsonToken) then - PurchaseLine.Validate("Direct Unit Cost", SimpleJsonHelper.GetJsonTokenDecimal(JsonToken)); - - PurchaseLine.Insert(true); - LineNo += 10000; - end; - end; - - // Return via RecordRef - CreatedDocumentHeader.GetTable(PurchaseHeader); - CreatedDocumentLines.GetTable(PurchaseLine); -end; -``` - -**βœ… Validation**: This will be tested in Exercise 2 when creating documents from received E-Documents. - ---- - -## πŸ”Œ Exercise 2: DirectionsConnector (30 minutes) - -In this exercise, you'll implement the **IDocumentSender** and **IDocumentReceiver** interfaces to send/receive documents via HTTP API. - -### Part A: Setup Connection (5 minutes) - -**Manual Setup**: -1. Open Business Central -2. Search for "Directions Connection Setup" -3. Enter the API Base URL: `[Provided by instructor]` -4. Enter your name (unique identifier) -5. Click "Register" to get your API key -6. Click "Test Connection" to verify - -**βœ… Validation**: You should see "Connection test successful!" - ---- - -### Part B: Send Document (10 minutes) - -**File**: `application/directions_connector/DirectionsIntegration.Codeunit.al` - -**Find**: The `Send()` procedure (around line 31) - -**Task**: Send an E-Document to the API /enqueue endpoint. - -**Implementation**: -```al -procedure Send(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; SendContext: Codeunit SendContext) -var - DirectionsSetup: Record "Directions Connection Setup"; - DirectionsAuth: Codeunit "Directions Auth"; - DirectionsRequests: Codeunit "Directions Requests"; - HttpClient: HttpClient; - HttpRequest: HttpRequestMessage; - HttpResponse: HttpResponseMessage; - TempBlob: Codeunit "Temp Blob"; - JsonContent: Text; -begin - // Get connection setup - DirectionsAuth.GetConnectionSetup(DirectionsSetup); - - // Get document content from SendContext - SendContext.GetTempBlob(TempBlob); - JsonContent := DirectionsRequests.ReadJsonFromBlob(TempBlob); - - // Create POST request to /enqueue - DirectionsRequests.CreatePostRequest(DirectionsSetup."API Base URL" + 'enqueue', JsonContent, HttpRequest); - - // Add authentication header - DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); - - // Log request (for E-Document framework logging) - SendContext.Http().SetHttpRequestMessage(HttpRequest); - - // Send request - if not HttpClient.Send(HttpRequest, HttpResponse) then - Error('Failed to send document to API.'); - - // Log response and check success - SendContext.Http().SetHttpResponseMessage(HttpResponse); - DirectionsRequests.CheckResponseSuccess(HttpResponse); -end; -``` - -**βœ… Validation**: -1. Create and post a Sales Invoice -2. Open the E-Document list -3. Send the E-Document (it should succeed) -4. Use the /peek endpoint in a browser to verify the document is in the queue - ---- - -### Part C: Receive Documents List (10 minutes) - -**File**: `application/directions_connector/DirectionsIntegration.Codeunit.al` - -**Find**: The `ReceiveDocuments()` procedure (around line 72) - -**Task**: Retrieve the list of available documents from the API /peek endpoint. - -**Implementation**: -```al -procedure ReceiveDocuments(var EDocumentService: Record "E-Document Service"; DocumentsMetadata: Codeunit "Temp Blob List"; ReceiveContext: Codeunit ReceiveContext) -var - DirectionsSetup: Record "Directions Connection Setup"; - DirectionsAuth: Codeunit "Directions Auth"; - DirectionsRequests: Codeunit "Directions Requests"; - HttpClient: HttpClient; - HttpRequest: HttpRequestMessage; - HttpResponse: HttpResponseMessage; - JsonObject: JsonObject; - JsonToken: JsonToken; - JsonArray: JsonArray; - TempBlob: Codeunit "Temp Blob"; - ResponseText: Text; - DocumentJson: Text; -begin - // Get connection setup - DirectionsAuth.GetConnectionSetup(DirectionsSetup); - - // Create GET request to /peek - DirectionsRequests.CreateGetRequest(DirectionsSetup."API Base URL" + 'peek', HttpRequest); - - // Add authentication - DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); - - // Log request - ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); - - // Send request - if not HttpClient.Send(HttpRequest, HttpResponse) then - Error('Failed to retrieve documents from API.'); - - // Log response and check success - ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); - DirectionsRequests.CheckResponseSuccess(HttpResponse); - - // Parse response and extract documents - ResponseText := DirectionsRequests.GetResponseText(HttpResponse); - if JsonObject.ReadFrom(ResponseText) then begin - if JsonObject.Get('items', JsonToken) then begin - JsonArray := JsonToken.AsArray(); - foreach JsonToken in JsonArray do begin - // Create a TempBlob for each document metadata - Clear(TempBlob); - JsonToken.WriteTo(DocumentJson); - DirectionsRequests.WriteTextToBlob(DocumentJson, TempBlob); - DocumentsMetadata.Add(TempBlob); - end; - end; - end; -end; -``` - -**βœ… Validation**: This will be tested when running "Get Documents" action in BC. - ---- - -### Part D: Download Single Document (5 minutes) - -**File**: `application/directions_connector/DirectionsIntegration.Codeunit.al` - -**Find**: The `DownloadDocument()` procedure (around line 135) - -**Task**: Download a single document from the API /dequeue endpoint. - -**Implementation**: -```al -procedure DownloadDocument(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; DocumentMetadata: codeunit "Temp Blob"; ReceiveContext: Codeunit ReceiveContext) -var - DirectionsSetup: Record "Directions Connection Setup"; - DirectionsAuth: Codeunit "Directions Auth"; - DirectionsRequests: Codeunit "Directions Requests"; - HttpClient: HttpClient; - HttpRequest: HttpRequestMessage; - HttpResponse: HttpResponseMessage; - JsonObject: JsonObject; - JsonToken: JsonToken; - TempBlob: Codeunit "Temp Blob"; - ResponseText: Text; - DocumentJson: Text; -begin - // Get connection setup - DirectionsAuth.GetConnectionSetup(DirectionsSetup); - - // Create GET request to /dequeue - DirectionsRequests.CreateGetRequest(DirectionsSetup."API Base URL" + 'dequeue', HttpRequest); - - // Add authentication - DirectionsAuth.AddAuthHeader(HttpRequest, DirectionsSetup); - - // Log request - ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); - - // Send request - if not HttpClient.Send(HttpRequest, HttpResponse) then - Error('Failed to download document from API.'); - - // Log response and check success - ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); - DirectionsRequests.CheckResponseSuccess(HttpResponse); - - // Parse response and extract document - ResponseText := DirectionsRequests.GetResponseText(HttpResponse); - if JsonObject.ReadFrom(ResponseText) then begin - if JsonObject.Get('document', JsonToken) then begin - JsonToken.WriteTo(DocumentJson); - ReceiveContext.GetTempBlob(TempBlob); - DirectionsRequests.WriteTextToBlob(DocumentJson, TempBlob); - end else - Error('No document found in response.'); - end; -end; -``` - -**βœ… Validation**: This will be tested in the complete flow below. - ---- - -## πŸ§ͺ Testing - Complete Flow (15 minutes) - -### Setup E-Document Service - -1. Open "E-Document Services" -2. Create a new service: - - **Code**: DIRECTIONS - - **Description**: Directions Connector - - **Document Format**: Simple JSON Format - - **Service Integration**: Directions Connector -3. Click "Setup Service Integration" and verify your connection -4. Enable the service - -### Test Outgoing Flow - -1. **Create Sales Invoice**: - - Customer: Any customer - - Add at least one line item - - Post the invoice - -2. **Send E-Document**: - - Open "E-Documents" list - - Find your posted invoice - - Action: "Send" - - Status should change to "Sent" - -3. **Verify in API**: - - Open browser: `[API Base URL]/peek` - - Add header: `X-Service-Key: [Your API Key]` - - You should see your document in the "items" array - -### Test Incoming Flow - -1. **Receive Documents**: - - Open "E-Document Services" - - Select your DIRECTIONS service - - Action: "Get Documents" - - New E-Documents should appear with status "Imported" - -2. **View Received Document**: - - Open the received E-Document - - Check the JSON content in the log - - Verify basic info is populated (vendor, date, etc.) - -3. **Create Purchase Invoice**: - - Action: "Create Document" - - A Purchase Invoice should be created - - Open the Purchase Invoice and verify: - - Vendor is set correctly - - Lines are populated with items, quantities, prices - - Dates and currency match - -4. **Verify Queue**: - - Check /peek endpoint again - - The queue should be empty (documents were dequeued) - ---- - -## πŸŽ‰ Success Criteria - -You have successfully completed the workshop if: -- βœ… You can post a Sales Invoice and see it as an E-Document -- βœ… The E-Document contains valid JSON -- βœ… You can send the E-Document to the API -- βœ… The document appears in the API queue -- βœ… You can receive documents from the API -- βœ… Purchase Invoices are created from received documents -- βœ… All data is mapped correctly - ---- - -## πŸ› Troubleshooting - -### "Failed to connect to API" -- Check the API Base URL in setup -- Verify the API server is running -- Check firewall settings - -### "Unauthorized or invalid key" -- Re-register in the setup page -- Verify the API key is saved -- Check that you're using the correct key header - -### "Document type not supported" -- Verify you selected "Simple JSON Format" in E-Document Service -- Check the format enum extension is compiled - -### "Failed to parse JSON" -- Check the JSON structure in E-Document log -- Verify all required fields are present -- Look for syntax errors (missing commas, brackets) - -### "Vendor does not exist" -- Create a vendor with the same number as the customer in the JSON -- Or modify the JSON to use an existing vendor number - ---- - -## πŸ“š Additional Resources - -- [E-Document Core README](../../README.md) -- [API Reference](../API_REFERENCE.md) -- [Workshop Plan](../WORKSHOP_PLAN.md) - ---- - -## πŸŽ“ Homework / Advanced Exercises - -Want to learn more? Try these: -1. Add support for Credit Memos -2. Implement `GetResponse()` for async status checking -3. Add custom fields to the JSON format -4. Implement validation rules for incoming documents -5. Add error handling and retry logic -6. Create a batch send function - ---- - -**Congratulations!** 🎊 You've completed the E-Document Connector Workshop! diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_INTRO.md b/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_INTRO.md deleted file mode 100644 index adc7446d..00000000 --- a/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_INTRO.md +++ /dev/null @@ -1,487 +0,0 @@ -# 🎯 E-Document Connector Workshop -## Directions EMEA 2025 - ---- - -## πŸ‘‹ Welcome! - -In the next **90 minutes**, you'll build a complete E-Document integration solution. - -**What you'll learn:** -- How the E-Document Core framework works -- How to implement format interfaces (JSON) -- How to implement integration interfaces (HTTP API) -- Complete round-trip document exchange - -**What you'll build:** -- βœ… SimpleJson Format - Convert documents to/from JSON -- βœ… DirectionsConnector - Send/receive via HTTP API - ---- - -## πŸ—οΈ Architecture Overview - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Business Central β”‚ -β”‚ β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Sales Invoice│───▢│ E-Document Core β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Format Interface│◀─── You implement! β”‚ -β”‚ β”‚ (SimpleJson) β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ JSON Blob β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Integration Interface │◀─── You implement! β”‚ -β”‚ β”‚ (DirectionsConnector) β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ HTTPS - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Azure API Server β”‚ - β”‚ β”‚ - β”‚ Queue Management β”‚ - β”‚ Document Storage β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Another Company / β”‚ - β”‚ Trading Partner β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - ---- - -## πŸ“¦ E-Document Core Framework - -The framework provides: - -### 1️⃣ **Document Interface** (`"E-Document"`) -Convert documents to/from Business Central - -**Outgoing** (BC β†’ External): -- `Check()` - Validate before sending -- `Create()` - Convert to format (JSON, XML, etc.) -- `CreateBatch()` - Batch multiple documents - -**Incoming** (External β†’ BC): -- `GetBasicInfoFromReceivedDocument()` - Parse metadata -- `GetCompleteInfoFromReceivedDocument()` - Create BC document - -### 2️⃣ **Integration Interfaces** -Send/receive documents via various channels - -**IDocumentSender**: -- `Send()` - Send document to external service - -**IDocumentReceiver**: -- `ReceiveDocuments()` - Get list of available documents -- `DownloadDocument()` - Download specific document - -**Others** (Advanced): -- `IDocumentResponseHandler` - Async status checking -- `ISentDocumentActions` - Approval/cancellation -- `IDocumentAction` - Custom actions - ---- - -## 🎨 What is SimpleJson? - -A simple JSON format for E-Documents designed for this workshop. - -### Example JSON Structure: - -```json -{ - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001", - "customerName": "Contoso Ltd.", - "postingDate": "2025-10-21", - "currencyCode": "USD", - "totalAmount": 1250.00, - "lines": [ - { - "lineNo": 1, - "type": "Item", - "no": "ITEM-001", - "description": "Item A", - "quantity": 5, - "unitPrice": 250.00, - "lineAmount": 1250.00 - } - ] -} -``` - -**Why JSON?** -- βœ… Human-readable -- βœ… Easy to parse -- βœ… Widely supported -- βœ… Perfect for learning - ---- - -## πŸ”Œ What is DirectionsConnector? - -An HTTP-based integration that sends/receives documents via REST API. - -### API Endpoints: - -| Endpoint | Method | Purpose | -|----------|--------|---------| -| `/register` | POST | Get API key | -| `/enqueue` | POST | Send document | -| `/peek` | GET | View queue | -| `/dequeue` | GET | Receive document | -| `/clear` | DELETE | Clear queue | - -### Authentication: -```http -X-Service-Key: your-api-key-here -``` - -### Why HTTP API? -- βœ… Simple and universal -- βœ… Easy to test (browser, Postman) -- βœ… Real-world scenario -- βœ… Stateless and scalable - ---- - -## πŸ”„ Complete Flow - -### Outgoing (Sending): -``` -Sales Invoice (BC) - ↓ Post -E-Document Created - ↓ Format: SimpleJson.Create() -JSON Blob - ↓ Integration: DirectionsConnector.Send() -HTTP POST /enqueue - ↓ -Azure API Queue -``` - -### Incoming (Receiving): -``` -Azure API Queue - ↓ Integration: DirectionsConnector.ReceiveDocuments() -HTTP GET /peek (list) - ↓ Integration: DirectionsConnector.DownloadDocument() -HTTP GET /dequeue (download) - ↓ -JSON Blob - ↓ Format: SimpleJson.GetBasicInfo() -E-Document Created (Imported) - ↓ Format: SimpleJson.GetCompleteInfo() -Purchase Invoice (BC) -``` - ---- - -## ⏱️ Workshop Timeline - -| Time | Duration | Activity | -|------|----------|----------| -| 00:00 | 10 min | ← You are here! (Introduction) | -| 00:10 | 30 min | **Exercise 1**: Implement SimpleJson Format | -| 00:40 | 30 min | **Exercise 2**: Implement DirectionsConnector | -| 01:10 | 15 min | Testing & Live Demo | -| 01:25 | 5 min | Wrap-up & Q&A | - ---- - -## πŸ“ Exercise 1: SimpleJson Format (30 min) - -Implement the **"E-Document" interface** - -### Part A: Check() - 5 minutes -Validate required fields before creating document -```al -procedure Check(var SourceDocumentHeader: RecordRef; ...) -begin - // TODO: Validate Customer No. - // TODO: Validate Posting Date -end; -``` - -### Part B: Create() - 15 minutes -Convert Sales Invoice to JSON -```al -procedure Create(...; var TempBlob: Codeunit "Temp Blob") -begin - // TODO: Generate JSON from Sales Invoice - // TODO: Include header and lines -end; -``` - -### Part C: GetBasicInfo() - 5 minutes -Parse incoming JSON metadata -```al -procedure GetBasicInfoFromReceivedDocument(var EDocument: Record "E-Document"; ...) -begin - // TODO: Parse JSON - // TODO: Set document type, number, date -end; -``` - -### Part D: GetCompleteInfo() - 5 minutes -Create Purchase Invoice from JSON -```al -procedure GetCompleteInfoFromReceivedDocument(...) -begin - // TODO: Parse JSON - // TODO: Create Purchase Header & Lines -end; -``` - ---- - -## πŸ”Œ Exercise 2: DirectionsConnector (30 min) - -Implement **IDocumentSender** and **IDocumentReceiver** interfaces - -### Part A: Setup - 5 minutes -Configure connection in BC -- API Base URL -- Register to get API Key -- Test connection - -### Part B: Send() - 10 minutes -Send document to API -```al -procedure Send(var EDocument: Record "E-Document"; ...) -begin - // TODO: Get JSON from SendContext - // TODO: POST to /enqueue endpoint - // TODO: Handle response -end; -``` - -### Part C: ReceiveDocuments() - 10 minutes -Get list of available documents -```al -procedure ReceiveDocuments(...; DocumentsMetadata: Codeunit "Temp Blob List"; ...) -begin - // TODO: GET from /peek endpoint - // TODO: Parse items array - // TODO: Add each to DocumentsMetadata list -end; -``` - -### Part D: DownloadDocument() - 5 minutes -Download specific document -```al -procedure DownloadDocument(var EDocument: Record "E-Document"; ...) -begin - // TODO: GET from /dequeue endpoint - // TODO: Parse response - // TODO: Store in TempBlob -end; -``` - ---- - -## 🎯 Success Criteria - -By the end, you should be able to: - -βœ… **Create** a Sales Invoice in BC -βœ… **Convert** it to JSON via SimpleJson format -βœ… **Send** it to Azure API via DirectionsConnector -βœ… **Verify** it appears in the queue -βœ… **Receive** documents from the API -βœ… **Parse** JSON and extract metadata -βœ… **Create** Purchase Invoices from received documents - -**You'll have built a complete E-Document integration!** πŸŽ‰ - ---- - -## πŸ› οΈ What's Pre-Written? - -To save time, these are already implemented: - -### SimpleJson Format: -- βœ… Extension setup (app.json) -- βœ… Enum extension -- βœ… Helper methods for JSON operations -- βœ… Error handling framework - -### DirectionsConnector: -- βœ… Connection Setup table & page -- βœ… Authentication helpers -- βœ… HTTP request builders -- βœ… Registration logic - -**You focus on the business logic!** - ---- - -## πŸ“‚ Your Workspace - -``` -application/ - β”œβ”€β”€ simple_json/ - β”‚ β”œβ”€β”€ app.json βœ… Pre-written - β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al βœ… Pre-written - β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al ⚠️ TODO sections - β”‚ └── SimpleJsonHelper.Codeunit.al βœ… Pre-written - β”‚ - └── directions_connector/ - β”œβ”€β”€ app.json βœ… Pre-written - β”œβ”€β”€ DirectionsIntegration.EnumExt.al βœ… Pre-written - β”œβ”€β”€ DirectionsIntegration.Codeunit.al ⚠️ TODO sections - β”œβ”€β”€ DirectionsConnectionSetup.Table.al βœ… Pre-written - β”œβ”€β”€ DirectionsConnectionSetup.Page.al βœ… Pre-written - β”œβ”€β”€ DirectionsAuth.Codeunit.al βœ… Pre-written - └── DirectionsRequests.Codeunit.al βœ… Pre-written -``` - ---- - -## πŸ“š Resources Available - -During the workshop: - -1. **WORKSHOP_GUIDE.md** - Step-by-step instructions with full code -2. **API_REFERENCE.md** - Complete API documentation -3. **README.md** (E-Document Core) - Framework reference -4. **Instructor** - Available for questions! - -After the workshop: -- Complete solution in `/solution/` folder -- Homework exercises -- Additional resources - ---- - -## πŸ’‘ Tips for Success - -1. **Read the TODO comments** - They contain hints and instructions -2. **Use the helper methods** - They're pre-written to save time -3. **Test incrementally** - Don't wait until the end -4. **Check the logs** - E-Document framework logs everything -5. **Ask questions** - The instructor is here to help! -6. **Have fun!** - This is a hands-on learning experience - ---- - -## πŸ› Common Pitfalls - -Watch out for: -- ❌ Missing commas in JSON -- ❌ Forgetting to add authentication headers -- ❌ Not handling empty lines/arrays -- ❌ Incorrect RecordRef table numbers -- ❌ Not logging HTTP requests/responses - -**The workshop guide has solutions for all of these!** - ---- - -## πŸŽ“ Beyond the Workshop - -After mastering the basics, explore: - -**Advanced Format Features:** -- Support multiple document types -- Add custom field mappings -- Implement validation rules -- Handle attachments (PDF, XML) - -**Advanced Integration Features:** -- Async status checking (`IDocumentResponseHandler`) -- Approval workflows (`ISentDocumentActions`) -- Batch processing -- Error handling and retry logic -- Custom actions (`IDocumentAction`) - -**Real-World Scenarios:** -- PEPPOL format -- Avalara integration -- Custom XML formats -- EDI integrations - ---- - -## 🀝 Workshop Collaboration - -### Partner Up! -- Work with a neighbor -- Share your API key to exchange documents -- Test each other's implementations - -### Group Testing -- Send documents to the group queue -- Everyone receives and processes them -- Great way to test at scale! - ---- - -## πŸš€ Let's Get Started! - -1. **Open** `WORKSHOP_GUIDE.md` for step-by-step instructions -2. **Navigate** to `application/simple_json/SimpleJsonFormat.Codeunit.al` -3. **Find** the first TODO section in the `Check()` method -4. **Start coding!** - -**Timer starts... NOW!** ⏰ - ---- - -## ❓ Questions? - -Before we start: -- ❓ Is everyone able to access the API URL? -- ❓ Does everyone have their development environment ready? -- ❓ Any questions about the architecture or flow? - ---- - -## πŸ“ž Need Help? - -During the workshop: -- πŸ™‹ Raise your hand -- πŸ’¬ Ask your neighbor -- πŸ“– Check the WORKSHOP_GUIDE.md -- πŸ” Look at the API_REFERENCE.md -- πŸ‘¨β€πŸ« Ask the instructor - -**We're all here to learn together!** - ---- - -# πŸŽ‰ Good Luck! - -**Remember**: The goal is to learn, not to finish first. -Take your time, experiment, and enjoy the process! - -**Now let's build something awesome!** πŸ’ͺ - ---- - -## Quick Links - -- πŸ“˜ [Workshop Guide](./WORKSHOP_GUIDE.md) - Step-by-step instructions -- πŸ”Œ [API Reference](./API_REFERENCE.md) - Endpoint documentation -- πŸ“ [Workshop Plan](./WORKSHOP_PLAN.md) - Overview and structure -- πŸ“š [E-Document Core README](../../../NAV_1/App/BCApps/src/Apps/W1/EDocument/App/README.md) - Framework docs - -**API Base URL**: `[Will be provided by instructor]` - ---- - -*Press `Ctrl+Shift+V` in VS Code to view this file in preview mode with formatting!* diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_PLAN.md b/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_PLAN.md deleted file mode 100644 index 0f343ff0..00000000 --- a/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_PLAN.md +++ /dev/null @@ -1,518 +0,0 @@ -# E-Document Connector Workshop - Implementation Plan - -## Workshop Overview -**Duration**: 90 minutes -**Format**: Hands-on coding workshop -**Goal**: Build a complete E-Document solution with SimpleJson format and DirectionsConnector integration - ---- - -## Timeline - -| Time | Duration | Activity | -|------|----------|----------| -| 00:00-00:10 | 10 min | Introduction (using VS Code as presentation) | -| 00:10-00:40 | 30 min | Exercise 1 - SimpleJson Format Implementation | -| 00:40-01:10 | 30 min | Exercise 2 - DirectionsConnector Integration | -| 01:10-01:25 | 15 min | Testing & Live Demo | -| 01:25-01:30 | 5 min | Wrap-up & Q&A | - ---- - -## Architecture Overview - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Business Central β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Sales Invoice │────────▢│ E-Document Core β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ SimpleJson Format β”‚ β”‚ -β”‚ β”‚ (Exercise 1) β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ JSON Blob β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ DirectionsConnector β”‚ β”‚ -β”‚ β”‚ (Exercise 2) β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ HTTP POST - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Azure API Server β”‚ - β”‚ (Pre-deployed) β”‚ - β”‚ β”‚ - β”‚ Endpoints: β”‚ - β”‚ - POST /register β”‚ - β”‚ - POST /enqueue β”‚ - β”‚ - GET /peek β”‚ - β”‚ - GET /dequeue β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - ---- - -## What Participants Will Build - -### Exercise 1: SimpleJson Format (30 minutes) -Implement the **"E-Document" Interface** to convert Business Central documents to/from JSON format. - -The interface has two main sections: -1. **Outgoing**: Convert BC documents to E-Document blobs (`Check`, `Create`, `CreateBatch`) -2. **Incoming**: Parse E-Document blobs to BC documents (`GetBasicInfoFromReceivedDocument`, `GetCompleteInfoFromReceivedDocument`) - -**Participants will implement:** -- βœ… `Check()` method - Validate required fields before document creation (5 min) -- βœ… `Create()` method - Generate JSON from Sales Invoice (15 min) -- βœ… `GetBasicInfoFromReceivedDocument()` method - Parse incoming JSON metadata (5 min) -- βœ… `GetCompleteInfoFromReceivedDocument()` method - Create Purchase Invoice from JSON (5 min) - -**Pre-written boilerplate includes:** -- Extension setup (app.json, dependencies) -- Enum extensions -- Helper methods for JSON generation and parsing -- Error handling framework - -### Exercise 2: DirectionsConnector Integration (30 minutes) -Implement the **Integration Interface** to send and receive documents via the Azure API server. - -**Participants will implement:** -- βœ… Connection setup and registration (5 min) -- βœ… `Send()` method - POST document to /enqueue endpoint (10 min) -- βœ… `ReceiveDocuments()` method - GET documents from /peek endpoint (10 min) -- βœ… `DownloadDocument()` method - GET single document from /dequeue endpoint (5 min) - -**Pre-written boilerplate includes:** -- Setup table and page UI -- Authentication helper -- HTTP request builders -- Error logging - ---- - -## Sample JSON Format - -Participants will generate JSON in this structure: - -```json -{ - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001", - "customerName": "Contoso Ltd.", - "postingDate": "2025-10-21", - "currencyCode": "USD", - "totalAmount": 1250.00, - "lines": [ - { - "lineNo": 1, - "type": "Item", - "no": "ITEM-001", - "description": "Item A", - "quantity": 5, - "unitPrice": 250.00, - "lineAmount": 1250.00 - } - ] -} -``` - ---- - -## Azure API Server - -**Base URL**: `https://[workshop-server].azurewebsites.net` (will be provided) - -### Endpoints - -#### 1. Register User -```http -POST /register -Content-Type: application/json - -{ - "name": "participant-name" -} - -Response: -{ - "status": "ok", - "key": "uuid-api-key" -} -``` - -#### 2. Send Document (Enqueue) -```http -POST /enqueue -X-Service-Key: your-api-key -Content-Type: application/json - -{ - "documentType": "Invoice", - "documentNo": "SI-001", - ... -} - -Response: -{ - "status": "ok", - "queued_count": 1 -} -``` - -#### 3. Check Queue -```http -GET /peek -X-Service-Key: your-api-key - -Response: -{ - "queued_count": 1, - "items": [...] -} -``` - -#### 4. Retrieve Document (Dequeue) -```http -GET /dequeue -X-Service-Key: your-api-key - -Response: -{ - "document": {...} -} -``` - ---- - -## Project Structure - -``` -DirectionsEMEA2025/ -β”œβ”€β”€ WORKSHOP_INTRO.md # VS Code presentation deck -β”œβ”€β”€ WORKSHOP_GUIDE.md # Step-by-step exercises -β”œβ”€β”€ API_REFERENCE.md # Azure API documentation -β”‚ -β”œβ”€β”€ application/ -β”‚ β”œβ”€β”€ simple_json/ # Exercise 1: Format Extension -β”‚ β”‚ β”œβ”€β”€ app.json -β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al -β”‚ β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al # ⚠️ TODO sections -β”‚ β”‚ └── SimpleJsonHelper.Codeunit.al # βœ… Pre-written -β”‚ β”‚ -β”‚ └── directions_connector/ # Exercise 2: Integration Extension -β”‚ β”œβ”€β”€ app.json -β”‚ β”œβ”€β”€ DirectionsIntegration.EnumExt.al -β”‚ β”œβ”€β”€ DirectionsIntegration.Codeunit.al # ⚠️ TODO sections -β”‚ β”œβ”€β”€ DirectionsSetup.Table.al # βœ… Pre-written -β”‚ β”œβ”€β”€ DirectionsSetup.Page.al # βœ… Pre-written -β”‚ β”œβ”€β”€ DirectionsAuth.Codeunit.al # βœ… Pre-written -β”‚ └── DirectionsRequests.Codeunit.al # βœ… Pre-written -β”‚ -β”œβ”€β”€ solution/ # Complete working solution -β”‚ β”œβ”€β”€ simple_json/ # Reference implementation -β”‚ └── directions_connector/ # Reference implementation -β”‚ -└── server/ - β”œβ”€β”€ server.py # FastAPI server (for reference) - β”œβ”€β”€ requirements.txt - └── README.md # Deployment info (Azure hosted) -``` - ---- - -## Deliverables - -### 1. WORKSHOP_INTRO.md -VS Code presentation covering: -- **Slide 1**: Welcome & Goals -- **Slide 2**: E-Document Framework Overview -- **Slide 3**: Architecture Diagram -- **Slide 4**: SimpleJson Format Introduction -- **Slide 5**: DirectionsConnector Overview -- **Slide 6**: Azure API Server -- **Slide 7**: Exercise Overview -- **Slide 8**: Success Criteria - -### 2. WORKSHOP_GUIDE.md -Step-by-step instructions: -- Prerequisites & setup -- **Exercise 1**: SimpleJson Format (implements "E-Document" interface) - - Part A: Implement Check() (5 min) - - Part B: Implement Create() (15 min) - - Part C: Implement GetBasicInfoFromReceivedDocument() (5 min) - - Part D: Implement GetCompleteInfoFromReceivedDocument() (5 min) - - Validation steps -- **Exercise 2**: DirectionsConnector - - Part A: Register & Setup (5 min) - - Part B: Implement Send() (10 min) - - Part C: Implement ReceiveDocuments() (10 min) - - Part D: Implement DownloadDocument() (5 min) - - Validation steps -- Testing instructions -- Troubleshooting guide - -### 3. API_REFERENCE.md -Complete API documentation: -- Base URL and authentication -- All endpoint specifications -- Request/response examples -- Error handling -- Rate limits (if any) - -### 4. SimpleJson Format Boilerplate -Files with TODO sections: -- `app.json` - Extension manifest with dependencies -- `SimpleJsonFormat.EnumExt.al` - Enum extension (pre-written) -- `SimpleJsonFormat.Codeunit.al` - Format implementation with TODOs: - ```al - // TODO: Exercise 1.A - Implement validation - procedure Check(...) - begin - // TODO: Validate Customer No. - // TODO: Validate Posting Date - // TODO: Validate at least one line exists - end; - - // TODO: Exercise 1.B - Generate JSON - procedure Create(...) - begin - // TODO: Create JSON header - // TODO: Add lines array - // TODO: Calculate totals - end; - - // TODO: Exercise 1.C - Parse incoming JSON (Basic Info) - procedure GetBasicInfoFromReceivedDocument(...) - begin - // TODO: Parse JSON from TempBlob - // TODO: Set EDocument."Document Type" - // TODO: Set EDocument."Bill-to/Pay-to No." - // TODO: Set EDocument."Bill-to/Pay-to Name" - // TODO: Set EDocument."Document Date" - // TODO: Set EDocument."Currency Code" - end; - - // TODO: Exercise 1.D - Create Purchase Invoice (Complete Info) - procedure GetCompleteInfoFromReceivedDocument(...) - begin - // TODO: Read JSON from TempBlob - // TODO: Create Purchase Header record - // TODO: Set header fields from JSON - // TODO: Create Purchase Lines from JSON array - // TODO: Return via RecordRef parameters - end; - ``` -- `SimpleJsonHelper.Codeunit.al` - Helper methods (pre-written): - - `AddJsonProperty()` - Add property to JSON - - `StartJsonObject()` - Start JSON object - - `StartJsonArray()` - Start JSON array - - `ParseJsonValue()` - Get value from JSON - - `GetJsonToken()` - Get JSON token by path - - etc. - -### 5. DirectionsConnector Boilerplate -Files with TODO sections: -- `app.json` - Extension manifest -- `DirectionsIntegration.EnumExt.al` - Enum extension (pre-written) -- `DirectionsIntegration.Codeunit.al` - Integration implementation with TODOs: - ```al - // TODO: Exercise 2.A - Setup connection - local procedure RegisterUser(...) // Provided with TODOs - - // TODO: Exercise 2.B - Send document - procedure Send(...) - begin - // TODO: Get connection setup - // TODO: Prepare HTTP request - // TODO: Set authorization header - // TODO: Send POST request - // TODO: Handle response - end; - - // TODO: Exercise 2.C - Receive documents list - procedure ReceiveDocuments(...) - begin - // TODO: Get connection setup - // TODO: Call /peek endpoint - // TODO: Parse response and create metadata blobs - end; - - // TODO: Exercise 2.D - Download single document - procedure DownloadDocument(...) - begin - // TODO: Read document ID from metadata - // TODO: Call /dequeue endpoint - // TODO: Store document content in TempBlob - end; - ``` -- Pre-written helper files: - - `DirectionsSetup.Table.al` - Connection settings (URL, API Key) - - `DirectionsSetup.Page.al` - Setup UI - - `DirectionsAuth.Codeunit.al` - Authentication helper - - `DirectionsRequests.Codeunit.al` - HTTP request builders - -### 6. Solution Folder -Complete working implementations for instructor reference: -- `/solution/simple_json/` - Fully implemented format -- `/solution/directions_connector/` - Fully implemented connector -- These are NOT given to participants initially - -### 7. Server Documentation -- `server/README.md` - Overview of the API server - - Architecture explanation - - Endpoint documentation - - Deployment notes (Azure hosted) - - No setup required (server is pre-deployed) - ---- - -## TODO Marking Convention - -In code files, use clear TODO markers with timing: - -```al -// ============================================================================ -// TODO: Exercise 1.A (10 minutes) -// Validate that required fields are filled before creating the document -// -// Instructions: -// 1. Validate that Customer No. is not empty -// 2. Validate that Posting Date is set -// 3. Validate that at least one line exists -// -// Hints: -// - Use SourceDocumentHeader.Field(FieldNo).TestField() -// - Use EDocumentErrorHelper.LogSimpleErrorMessage() for custom errors -// - Check the README.md for the Check() method example -// ============================================================================ -procedure Check(var SourceDocumentHeader: RecordRef; EDocumentService: Record "E-Document Service"; EDocumentProcessingPhase: Enum "E-Document Processing Phase") -begin - // TODO: Your code here -end; -``` - ---- - -## Success Criteria - -By the end of the workshop, participants should be able to: -- βœ… Create a sales invoice in BC -- βœ… See it converted to JSON via SimpleJson format -- βœ… Send it to Azure API server via DirectionsConnector -- βœ… Verify it appears in the queue (via /peek endpoint) -- βœ… Receive documents from the Azure API server -- βœ… Parse incoming JSON and extract metadata -- βœ… Create purchase invoices from received E-Documents -- βœ… Understand the complete E-Document round-trip flow (outgoing and incoming) -- βœ… Understand the E-Document framework architecture -- βœ… Know how to extend with additional features - ---- - -## Bonus/Homework Ideas - -For advanced participants or post-workshop: - -**Format Interface:** -- Implement `CreateBatch()` for batch processing multiple documents -- Add support for Sales Credit Memos and Purchase Credit Memos -- Add more sophisticated field mappings (dimensions, custom fields) -- Implement validation rules for incoming documents -- Support for attachments (PDF, XML) - -**Integration Interface:** -- Implement `IDocumentResponseHandler` with `GetResponse()` for async status checking -- Implement `ISentDocumentActions` for approval/cancellation workflows -- Implement `IDocumentAction` for custom actions -- Add comprehensive error handling and retry logic -- Add batch sending support - ---- - -## Notes for Implementation - -### Key Simplifications for 90-Minute Workshop -1. **Format**: Full round-trip (Create and PrepareDocument), but simplified field mapping -2. **Connector**: Full round-trip (Send and Receive), but simplified response handling -3. **Validation**: Basic field checks only -4. **Error Handling**: Use pre-written helpers -5. **UI**: Minimal - focus on code logic -6. **Document Types**: Only Sales Invoice outgoing, only Purchase Invoice incoming - -### Pre-Written vs TODO -**Participants write** (~60% of time): -- Business validation logic -- JSON structure generation -- HTTP request preparation -- Response handling - -**Pre-written boilerplate** (~40% setup time saved): -- Extension setup and dependencies -- Enum extensions -- Setup tables/pages -- Helper methods (JSON, HTTP, Auth) -- Error logging framework - -### Testing Strategy - -**Outgoing Flow (Send):** -1. Create test sales invoice with known data -2. Post and verify E-Document created -3. Check JSON blob content in E-Document log -4. Verify document sent to Azure API -5. Use /peek endpoint to confirm receipt in queue - -**Incoming Flow (Receive):** -6. Use another participant's queue or test data in API -7. Run "Get Documents" action in BC -8. Verify E-Documents appear in E-Document list with status "Imported" -9. Check downloaded JSON content in E-Document log -10. Verify basic info parsed correctly (vendor, amount, date) -11. Create Purchase Invoice from E-Document -12. Verify purchase invoice created with correct data -13. Confirm document removed from queue (via /peek) - ---- - -## Files to Create - -### Immediate (for workshop) -- [ ] `WORKSHOP_INTRO.md` - VS Code presentation -- [ ] `WORKSHOP_GUIDE.md` - Exercise instructions -- [ ] `API_REFERENCE.md` - Azure API docs -- [ ] `application/simple_json/` - Format boilerplate -- [ ] `application/directions_connector/` - Connector boilerplate - -### Reference (for instructor) -- [ ] `solution/simple_json/` - Complete format -- [ ] `solution/directions_connector/` - Complete connector -- [ ] `server/README.md` - Server documentation - -### Nice-to-have -- [ ] `TROUBLESHOOTING.md` - Common issues -- [ ] `HOMEWORK.md` - Post-workshop exercises -- [ ] Sample test data scripts - ---- - -## Next Steps - -1. Review and approve this plan -2. Get Azure server URL and deployment details -3. Create the workshop introduction (WORKSHOP_INTRO.md) -4. Create the boilerplate code with TODOs -5. Create the complete solution for reference -6. Test the full workshop flow -7. Prepare any PowerPoint backup slides (if needed) - ---- - -**Ready to implement?** Let me know if you want to adjust anything before we start building! From f6b6bbc1789060bb93e715ad4e809e5196d60fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Mon, 27 Oct 2025 23:59:29 +0100 Subject: [PATCH 7/9] Fixes --- .../ConnectorAuth.Codeunit.al | 6 +- .../ConnectorConnectionSetup.Table.al | 2 +- .../DirectionsEMEA2025/server/app.py | 66 ++++++++++--------- .../server/requirements.txt | 4 +- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorAuth.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorAuth.Codeunit.al index 8a2e50ef..f4f2a1d4 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorAuth.Codeunit.al +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorAuth.Codeunit.al @@ -40,16 +40,14 @@ codeunit 50121 "Connector Auth" JsonObject.WriteTo(RequestBody); // Prepare HTTP request - HttpRequest.GetHeaders(HttpHeaders); + HttpRequest.Content.WriteFrom(RequestBody); + HttpRequest.Content.GetHeaders(HttpHeaders); if HttpHeaders.Contains('Content-Type') then HttpHeaders.Remove('Content-Type'); HttpHeaders.Add('Content-Type', 'application/json'); - HttpContent.WriteFrom(RequestBody); - HttpRequest.Method := 'POST'; HttpRequest.SetRequestUri(ConnectorSetup."API Base URL" + 'register'); - HttpRequest.Content := HttpContent; // Send request if not HttpClient.Send(HttpRequest, HttpResponse) then diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Table.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Table.al index 6f7c7d10..7f4136e7 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Table.al +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Table.al @@ -89,6 +89,6 @@ table 50122 "Connector Connection Setup" /// procedure GetAPIKeyText(): Text begin - exit(Format("API Key")); + exit(Format("API Key").Replace('{', '').Replace('}', '')); end; } diff --git a/samples/EDocument/DirectionsEMEA2025/server/app.py b/samples/EDocument/DirectionsEMEA2025/server/app.py index ab6bda14..9b461f18 100644 --- a/samples/EDocument/DirectionsEMEA2025/server/app.py +++ b/samples/EDocument/DirectionsEMEA2025/server/app.py @@ -1,55 +1,61 @@ -from fastapi import FastAPI, Request, HTTPException +from flask import Flask, request, jsonify, abort from collections import defaultdict, deque import uuid -app = FastAPI() +app = Flask(__name__) # Simple in-memory stores auth_keys = {} # user_id -> key queues = defaultdict(deque) # key -> deque -@app.post("/register") -async def register(request: Request): - data = await request.json() +@app.route("/register", methods=["POST"]) +def register(): + data = request.get_json() name = data.get("name") if not name: - raise HTTPException(400, "Missing name") + abort(400, "Missing name") key = str(uuid.uuid4()) auth_keys[name] = key queues[key] # Initialize queue - return {"status": "ok", "key": key} + return jsonify({"status": "ok", "key": key}) -def get_key(request: Request) -> str: - key = request.headers.get("X-Service-Key") +def get_key() -> str: + key = str.lower(request.headers.get("X-Service-Key")) if not key or key not in queues: - raise HTTPException(401, "Unauthorized or invalid key") + abort(401, "Unauthorized or invalid key") return key -@app.post("/enqueue") -async def enqueue(request: Request): - key = get_key(request) - doc = await request.json() +@app.route("/enqueue", methods=["POST"]) +def enqueue(): + key = get_key() + doc = request.get_json() queues[key].append(doc) - return {"status": "ok", "queued_count": len(queues[key])} + return jsonify({"status": "ok", "queued_count": len(queues[key])}) -@app.get("/dequeue") -async def dequeue(request: Request): - key = get_key(request) +@app.route("/dequeue", methods=["GET"]) +def dequeue(): + key = get_key() if not queues[key]: - raise HTTPException(404, "Queue empty") - return {"document": queues[key].popleft()} + abort(404, "Queue empty") + return jsonify({"document": queues[key].popleft()}) -@app.get("/peek") -async def peek(request: Request): - key = get_key(request) - return {"queued_count": len(queues[key]), "items": list(queues[key])} +@app.route("/peek", methods=["GET"]) +def peek(): + key = get_key() + return jsonify({"queued_count": len(queues[key]), "items": list(queues[key])}) -@app.delete("/clear") -async def clear(request: Request): - key = get_key(request) +@app.route("/clear", methods=["DELETE"]) +def clear(): + key = get_key() queues[key].clear() - return {"status": "cleared"} + return jsonify({"status": "cleared"}) -@app.get("/") -async def root(): +@app.route("/") +def root(): return "Hello world" + +# if __name__ == "__main__": +# app.run(debug=True, host="0.0.0.0", port=8000) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/samples/EDocument/DirectionsEMEA2025/server/requirements.txt b/samples/EDocument/DirectionsEMEA2025/server/requirements.txt index 8e0578a0..809745ba 100644 --- a/samples/EDocument/DirectionsEMEA2025/server/requirements.txt +++ b/samples/EDocument/DirectionsEMEA2025/server/requirements.txt @@ -1,2 +1,2 @@ -fastapi -uvicorn[standard] \ No newline at end of file +flask==3.0.0 +werkzeug==3.0.1 \ No newline at end of file From 3405eebb79c435ee55ef9cb1dcf46c648046757a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Tue, 28 Oct 2025 09:59:30 +0100 Subject: [PATCH 8/9] Fixes --- .../ConnectorIntegration.Codeunit.al | 29 +- .../ConnectorRequests.Codeunit.al | 5 +- .../simple_json/SimpleJsonFormat.Codeunit.al | 75 ++- .../simple_json/SimpleJsonFormat.EnumExt.al | 2 +- .../simple_json/SimpleJsonHelper.Codeunit.al | 15 + .../DirectionsEMEA2025/server/app.py | 3 +- .../workshop/API_REFERENCE.md | 476 --------------- .../workshop/COMPLETE_WORKSHOP_GUIDE.md | 564 ------------------ .../workshop/WORKSHOP_GUIDE.md | 0 .../workshop/docprofile.png | Bin 0 -> 165435 bytes .../DirectionsEMEA2025/workshop/service.png | Bin 0 -> 65117 bytes .../DirectionsEMEA2025/workshop/workflow.png | Bin 0 -> 59278 bytes 12 files changed, 84 insertions(+), 1085 deletions(-) delete mode 100644 samples/EDocument/DirectionsEMEA2025/workshop/COMPLETE_WORKSHOP_GUIDE.md create mode 100644 samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_GUIDE.md create mode 100644 samples/EDocument/DirectionsEMEA2025/workshop/docprofile.png create mode 100644 samples/EDocument/DirectionsEMEA2025/workshop/service.png create mode 100644 samples/EDocument/DirectionsEMEA2025/workshop/workflow.png diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.Codeunit.al index 13737c2b..1bdf645e 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.Codeunit.al +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.Codeunit.al @@ -23,8 +23,8 @@ codeunit 50123 "Connector Integration" implements IDocumentSender, IDocumentRece // ============================================================================ // ============================================================================ - // TODO: Exercise 2.A (10 minutes) - // Send an E-Document to the Connector API. + // TODO: Exercise 2 (10 minutes) + // Now send the SimpleJSON to the endpoint, using the Connector API. // // TASK: Do the TODOs in the Send procedure below // ============================================================================ @@ -45,18 +45,24 @@ codeunit 50123 "Connector Integration" implements IDocumentSender, IDocumentRece // - Tips: Use SendContext.GetTempBlob() // - Tips: Use ConnectorRequests.ReadJsonFromBlob(TempBlob) to read json text from blob + + TempBlob := SendContext.GetTempBlob(); + JsonContent := ConnectorRequests.ReadJsonFromBlob(TempBlob); // // TODO: Create POST request to 'enqueue' endpoint // - Tips: Add enqueue to the base URL from ConnectorSetup // + APIEndpoint := ConnectorSetup."API Base URL" + 'enqueue'; ConnectorRequests.CreatePostRequest(APIEndpoint, JsonContent, HttpRequest); ConnectorAuth.AddAuthHeader(HttpRequest, ConnectorSetup); // TODO: Send the HTTP request and handle the response using HttpClient - + + if not HttpClient.Send(HttpRequest, HttpResponse) then + Error('Failed to connect to the API server.'); // SendContext.Http().SetHttpRequestMessage(HttpRequest); @@ -69,7 +75,7 @@ codeunit 50123 "Connector Integration" implements IDocumentSender, IDocumentRece // ============================================================================ // ============================================================================ - // TODO: Exercise 2.B (10 minutes) + // TODO: Exercise 3,A (10 minutes) // Receive a list of documents from the Connector API. // // TASK: Do the todos @@ -95,6 +101,7 @@ codeunit 50123 "Connector Integration" implements IDocumentSender, IDocumentRece // TODO: Create Get request to 'peek' endpoint // - Tips: Add peek to the base URL from ConnectorSetup + APIEndpoint := ConnectorSetup."API Base URL" + 'peek'; // @@ -103,10 +110,11 @@ codeunit 50123 "Connector Integration" implements IDocumentSender, IDocumentRece // TODO: Send the HTTP request and handle the response using HttpClient - + + if not HttpClient.Send(HttpRequest, HttpResponse) then + Error('Failed to connect to the API server.'); // - ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); ConnectorRequests.CheckResponseSuccess(HttpResponse); @@ -122,6 +130,7 @@ codeunit 50123 "Connector Integration" implements IDocumentSender, IDocumentRece ConnectorRequests.WriteTextToBlob(DocumentJson, TempBlob); // TODO: Add TempBlob to DocumentsMetadata so we can process it later in DownloadDocument + DocumentsMetadata.Add(TempBlob); // end; end; @@ -129,7 +138,7 @@ codeunit 50123 "Connector Integration" implements IDocumentSender, IDocumentRece end; // ============================================================================ - // TODO: Exercise 2.C (5 minutes) + // TODO: Exercise 3.B (5 minutes) // Download a single document from the Connector API (dequeue). // // TASK: Do the todos @@ -154,14 +163,16 @@ codeunit 50123 "Connector Integration" implements IDocumentSender, IDocumentRece // TODO: Create Get request to 'dequeue' endpoint // - Tips: Add dequeue to the base URL from ConnectorSetup - // ` + APIEndpoint := ConnectorSetup."API Base URL" + 'dequeue'; + // ConnectorRequests.CreateGetRequest(APIEndpoint, HttpRequest); ConnectorAuth.AddAuthHeader(HttpRequest, ConnectorSetup); // TODO: Send the HTTP request and handle the response using HttpClient // - + if not HttpClient.Send(HttpRequest, HttpResponse) then + Error('Failed to connect to the API server.'); ReceiveContext.Http().SetHttpRequestMessage(HttpRequest); ReceiveContext.Http().SetHttpResponseMessage(HttpResponse); diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorRequests.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorRequests.Codeunit.al index 911d2a56..d11adc14 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorRequests.Codeunit.al +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorRequests.Codeunit.al @@ -22,16 +22,15 @@ codeunit 50125 "Connector Requests" HttpHeaders: HttpHeaders; HttpContent: HttpContent; begin - HttpContent.WriteFrom(JsonContent); + HttpRequest.Content.WriteFrom(JsonContent); + HttpRequest.Content.GetHeaders(HttpHeaders); // Prepare HTTP request - HttpRequest.GetHeaders(HttpHeaders); if HttpHeaders.Contains('Content-Type') then HttpHeaders.Remove('Content-Type'); HttpHeaders.Add('Content-Type', 'application/json'); HttpRequest.Method := 'POST'; HttpRequest.SetRequestUri(Url); - HttpRequest.Content := HttpContent; end; /// diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al index b6df7d73..b0ba7bfa 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.Codeunit.al @@ -33,7 +33,7 @@ codeunit 50102 "SimpleJson Format" implements "E-Document" SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Sell-to Customer No.")).TestField(); // TODO: Exercise 1.A: Validate Posting Date - + SourceDocumentHeader.Field(SalesInvoiceHeader.FieldNo("Posting Date")).TestField(); end; end; end; @@ -72,7 +72,14 @@ codeunit 50102 "SimpleJson Format" implements "E-Document" RootObject.Add('currencyCode', SalesInvoiceHeader."Currency Code"); RootObject.Add('totalAmount', Format(SalesInvoiceHeader."Amount Including VAT", 0, 9)); - // TODO: Exercise 1.B - Fill in customerNo and customerName for header + // TODO: Exercise 1.B - Fill in customerNo and customerName, vendorNo and VendorName for header + // It is important that you have a vendor with the same number in your system, since when you receive the data you will have to pick the vendor based on the VendorNo + RootObject.Add('customerNo', SalesInvoiceHeader."Sell-to Customer No."); + RootObject.Add('customerName', SalesInvoiceHeader."Sell-to Customer Name"); + + // Hardcoded for simplicity. Normally this would be company information + RootObject.Add('vendorNo', '10000'); + RootObject.Add('vendorName', 'Adatum Corporation'); // Create lines array if SalesInvoiceLine.FindSet() then @@ -85,8 +92,8 @@ codeunit 50102 "SimpleJson Format" implements "E-Document" LineObject.Add('lineAmount', SalesInvoiceLine."Amount Including VAT"); // TODO: Exercise 1.B - Fill in description and quantity for line - - + LineObject.Add('description', SalesInvoiceLine.Description); + LineObject.Add('quantity', SalesInvoiceLine.Quantity); LinesArray.Add(LineObject); until SalesInvoiceLine.Next() = 0; @@ -104,7 +111,7 @@ codeunit 50102 "SimpleJson Format" implements "E-Document" // ============================================================================ // INCOMING DOCUMENTS - // Exercise 2 + // Exercise 4 // Parse information from received JSON document. // ============================================================================ @@ -122,8 +129,8 @@ codeunit 50102 "SimpleJson Format" implements "E-Document" // Extract document number if SimpleJsonHelper.SelectJsonToken(JsonObject, 'documentNo', JsonToken) then - EDocument."Document No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Document No.")); - + EDocument."Incoming E-Document No." := CopyStr(SimpleJsonHelper.GetJsonTokenValue(JsonToken), 1, MaxStrLen(EDocument."Incoming E-Document No.")); + // Extract posting date if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then EDocument."Document Date" := SimpleJsonHelper.GetJsonTokenDate(JsonToken); @@ -135,21 +142,23 @@ codeunit 50102 "SimpleJson Format" implements "E-Document" // TODO: Exercise 2.A - Fill in the vendor information and total amount - // TODO: Extract vendor number (from "customerNo" in JSON) - if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then - EDocument."Bill-to/Pay-to No." := ''; - // TODO: Extract vendor name - if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then - EDocument."Bill-to/Pay-to Name" := ''; - // TODO: Extract total amount - if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then - EDocument."Amount Incl. VAT" := 0; + // TODO: Extract vendor number (from "vendorNo" in JSON) + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'vendorNo', JsonToken) then + EDocument."Bill-to/Pay-to No." := SimpleJsonHelper.getJsonTokenValue(JsonToken); + + // TODO: Extract vendor name + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'vendorName', JsonToken) then + EDocument."Bill-to/Pay-to Name" := SimpleJsonHelper.getJsonTokenValue(JsonToken); + + // TODO: Extract total amount + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'totalAmount', JsonToken) then + EDocument."Amount Incl. VAT" := SimpleJsonHelper.GetJsonTokenDecimal(JsonToken); end; procedure GetCompleteInfoFromReceivedDocument(var EDocument: Record "E-Document"; var CreatedDocumentHeader: RecordRef; var CreatedDocumentLines: RecordRef; var TempBlob: Codeunit "Temp Blob") var - PurchaseHeader: Record "Purchase Header"; - PurchaseLine: Record "Purchase Line"; + PurchaseHeader: Record "Purchase Header" temporary; + PurchaseLine: Record "Purchase Line" temporary; JsonObject: JsonObject; JsonToken: JsonToken; JsonArray: JsonArray; @@ -161,17 +170,17 @@ codeunit 50102 "SimpleJson Format" implements "E-Document" Error('Failed to parse JSON document'); // Create Purchase Header - PurchaseHeader.Init(); PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::Invoice; - PurchaseHeader.Insert(); + PurchaseHeader.InitRecord(); + PurchaseHeader.Insert(true); // Set vendor from JSON - if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then - PurchaseHeader.Validate("Buy-from Vendor No.", ''); + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'vendorNo', JsonToken) then + PurchaseHeader.Validate("Buy-from Vendor No.", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); // Set posting date - if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then - PurchaseHeader.Validate("Posting Date", 0D); + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'postingDate', JsonToken) then + PurchaseHeader.Validate("Posting Date", SimpleJsonHelper.GetJsonTokenDate(JsonToken)); // Set currency code (if not blank) if SimpleJsonHelper.SelectJsonToken(JsonObject, 'currencyCode', JsonToken) then begin @@ -179,7 +188,7 @@ codeunit 50102 "SimpleJson Format" implements "E-Document" PurchaseHeader.Validate("Currency Code", SimpleJsonHelper.GetJsonTokenValue(JsonToken)); end; - PurchaseHeader.Modify(); + PurchaseHeader.Modify(true); // Create Purchase Lines from JSON array if JsonObject.Get('lines', JsonToken) then begin @@ -199,16 +208,20 @@ codeunit 50102 "SimpleJson Format" implements "E-Document" PurchaseLine."No." := SimpleJsonHelper.GetJsonTokenValue(JsonToken); // TODO: Set description - if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then - PurchaseLine.Description := ''; + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'description', JsonToken) then + PurchaseLine.Description := SimpleJsonHelper.GetJsonTokenValue(JsonToken); // TODO: Set quantity - if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then - PurchaseLine.Quantity := 0; + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'quantity', JsonToken) then + PurchaseLine.Quantity := SimpleJsonHelper.GetJsonTokenDecimal(JsonToken); + + // TODO: Set line amount + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'lineAmount', JsonToken) then + PurchaseLine.Amount := SimpleJsonHelper.GetJsonTokenDecimal(JsonToken); // Set unit cost - if SimpleJsonHelper.SelectJsonToken(JsonObject, '???', JsonToken) then - PurchaseLine."Direct Unit Cost" := 0; + if SimpleJsonHelper.SelectJsonToken(JsonObject, 'unitCost', JsonToken) then + PurchaseLine."Direct Unit Cost" := SimpleJsonHelper.GetJsonTokenDecimal(JsonToken); PurchaseLine.Insert(true); LineNo += 10000; diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.EnumExt.al b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.EnumExt.al index 43581a41..86f1c9fe 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.EnumExt.al +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonFormat.EnumExt.al @@ -14,7 +14,7 @@ enumextension 50100 "SimpleJson Format" extends "E-Document Format" { value(50100; "SimpleJson") { - Caption = 'Simple JSON Format - Exercise 1'; + Caption = 'Simple JSON'; Implementation = "E-Document" = "SimpleJson Format"; } } diff --git a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonHelper.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonHelper.Codeunit.al index 682dc160..c678680e 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonHelper.Codeunit.al +++ b/samples/EDocument/DirectionsEMEA2025/application/simple_json/SimpleJsonHelper.Codeunit.al @@ -5,6 +5,7 @@ namespace Microsoft.EServices.EDocument.Format; using System.Utilities; +using Microsoft.eServices.EDocument; /// /// Helper codeunit with pre-written methods for JSON operations. @@ -98,4 +99,18 @@ codeunit 50104 "SimpleJson Helper" InStr.ReadText(JsonText); exit(JsonObject.ReadFrom(JsonText)); end; + + + [EventSubscriber(ObjectType::Table, Database::"E-Document Log", OnBeforeExportDataStorage, '', false, false)] + local procedure OnBeforeExportDataStorage(EDocumentLog: Record "E-Document Log"; var FileName: Text) + var + EDocumentService: Record "E-Document Service"; + begin + EDocumentService.Get(EDocumentLog."Service Code"); + if EDocumentService."Document Format" <> EDocumentService."Document Format"::"SimpleJson" then + exit; + + FileName := StrSubstNo('%1_%2.json', EDocumentLog."Service Code", EDocumentLog."Document No."); + end; + } diff --git a/samples/EDocument/DirectionsEMEA2025/server/app.py b/samples/EDocument/DirectionsEMEA2025/server/app.py index 9b461f18..62c993f2 100644 --- a/samples/EDocument/DirectionsEMEA2025/server/app.py +++ b/samples/EDocument/DirectionsEMEA2025/server/app.py @@ -37,7 +37,8 @@ def dequeue(): key = get_key() if not queues[key]: abort(404, "Queue empty") - return jsonify({"document": queues[key].popleft()}) + value = queues[key].popleft() + return jsonify({"document": value}) @app.route("/peek", methods=["GET"]) def peek(): diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/API_REFERENCE.md b/samples/EDocument/DirectionsEMEA2025/workshop/API_REFERENCE.md index 1652af87..e69de29b 100644 --- a/samples/EDocument/DirectionsEMEA2025/workshop/API_REFERENCE.md +++ b/samples/EDocument/DirectionsEMEA2025/workshop/API_REFERENCE.md @@ -1,476 +0,0 @@ -# Directions Connector API Reference - -Complete API documentation for the workshop server hosted on Azure. - ---- - -## Base URL - -``` -https://[workshop-server].azurewebsites.net/ -``` - -**Note**: The actual URL will be provided by the instructor at the start of the workshop. - ---- - -## Authentication - -All endpoints (except `/register`) require authentication via the `X-Service-Key` header. - -```http -X-Service-Key: your-api-key-here -``` - -The API key is obtained by calling the `/register` endpoint. - ---- - -## Endpoints - -### 1. Register User - -Register a new user and receive an API key. - -**Endpoint**: `POST /register` - -**Authentication**: None - -**Request**: -```http -POST /register HTTP/1.1 -Content-Type: application/json - -{ - "name": "participant-name" -} -``` - -**Response** (200 OK): -```json -{ - "status": "ok", - "key": "12345678-1234-1234-1234-123456789abc" -} -``` - -**Example (cURL)**: -```bash -curl -X POST https://workshop-server.azurewebsites.net/register \ - -H "Content-Type: application/json" \ - -d '{"name": "john-doe"}' -``` - -**Example (PowerShell)**: -```powershell -$body = @{ name = "john-doe" } | ConvertTo-Json -Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/register" ` - -Method Post ` - -Body $body ` - -ContentType "application/json" -``` - -**Notes**: -- Each name should be unique to avoid conflicts -- The API key is returned only once - save it securely -- If you lose your key, you need to register again with a different name - ---- - -### 2. Send Document (Enqueue) - -Add a document to your queue. - -**Endpoint**: `POST /enqueue` - -**Authentication**: Required (`X-Service-Key` header) - -**Request**: -```http -POST /enqueue HTTP/1.1 -X-Service-Key: your-api-key -Content-Type: application/json - -{ - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001", - "customerName": "Contoso Ltd.", - "postingDate": "2025-10-21", - "currencyCode": "USD", - "totalAmount": 1250.00, - "lines": [ - { - "lineNo": 1, - "type": "Item", - "no": "ITEM-001", - "description": "Item A", - "quantity": 5, - "unitPrice": 250.00, - "lineAmount": 1250.00 - } - ] -} -``` - -**Response** (200 OK): -```json -{ - "status": "ok", - "queued_count": 3 -} -``` - -**Example (cURL)**: -```bash -curl -X POST https://workshop-server.azurewebsites.net/enqueue \ - -H "X-Service-Key: your-api-key" \ - -H "Content-Type: application/json" \ - -d '{ - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001", - "customerName": "Contoso Ltd.", - "postingDate": "2025-10-21", - "currencyCode": "USD", - "totalAmount": 1250.00, - "lines": [ - { - "lineNo": 1, - "type": "Item", - "no": "ITEM-001", - "description": "Item A", - "quantity": 5, - "unitPrice": 250.00, - "lineAmount": 1250.00 - } - ] - }' -``` - -**Example (PowerShell)**: -```powershell -$headers = @{ - "X-Service-Key" = "your-api-key" - "Content-Type" = "application/json" -} - -$body = @{ - documentType = "Invoice" - documentNo = "SI-001" - customerNo = "C001" - customerName = "Contoso Ltd." - postingDate = "2025-10-21" - currencyCode = "USD" - totalAmount = 1250.00 - lines = @( - @{ - lineNo = 1 - type = "Item" - no = "ITEM-001" - description = "Item A" - quantity = 5 - unitPrice = 250.00 - lineAmount = 1250.00 - } - ) -} | ConvertTo-Json -Depth 10 - -Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/enqueue" ` - -Method Post ` - -Headers $headers ` - -Body $body -``` - -**Notes**: -- The document is added to your personal queue (isolated by API key) -- You can enqueue any valid JSON document structure -- The `queued_count` returns the total number of documents in your queue - ---- - -### 3. Check Queue (Peek) - -View all documents in your queue without removing them. - -**Endpoint**: `GET /peek` - -**Authentication**: Required (`X-Service-Key` header) - -**Request**: -```http -GET /peek HTTP/1.1 -X-Service-Key: your-api-key -``` - -**Response** (200 OK): -```json -{ - "queued_count": 2, - "items": [ - { - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001", - "customerName": "Contoso Ltd.", - "postingDate": "2025-10-21", - "currencyCode": "USD", - "totalAmount": 1250.00, - "lines": [...] - }, - { - "documentType": "Invoice", - "documentNo": "SI-002", - "customerNo": "C002", - "customerName": "Fabrikam Inc.", - "postingDate": "2025-10-21", - "currencyCode": "EUR", - "totalAmount": 850.00, - "lines": [...] - } - ] -} -``` - -**Example (cURL)**: -```bash -curl -X GET https://workshop-server.azurewebsites.net/peek \ - -H "X-Service-Key: your-api-key" -``` - -**Example (PowerShell)**: -```powershell -$headers = @{ "X-Service-Key" = "your-api-key" } -Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/peek" ` - -Method Get ` - -Headers $headers -``` - -**Example (Browser)**: -You can also use browser extensions like "Modify Header Value" to add the header and view in browser: -``` -https://workshop-server.azurewebsites.net/peek -Header: X-Service-Key: your-api-key -``` - -**Notes**: -- Documents remain in the queue after peeking -- Useful for debugging and verifying document submission -- Returns all documents in your queue (FIFO order) - ---- - -### 4. Retrieve Document (Dequeue) - -Retrieve and remove the first document from your queue. - -**Endpoint**: `GET /dequeue` - -**Authentication**: Required (`X-Service-Key` header) - -**Request**: -```http -GET /dequeue HTTP/1.1 -X-Service-Key: your-api-key -``` - -**Response** (200 OK): -```json -{ - "document": { - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001", - "customerName": "Contoso Ltd.", - "postingDate": "2025-10-21", - "currencyCode": "USD", - "totalAmount": 1250.00, - "lines": [ - { - "lineNo": 1, - "type": "Item", - "no": "ITEM-001", - "description": "Item A", - "quantity": 5, - "unitPrice": 250.00, - "lineAmount": 1250.00 - } - ] - } -} -``` - -**Response** (404 Not Found) - Queue Empty: -```json -{ - "detail": "Queue empty" -} -``` - -**Example (cURL)**: -```bash -curl -X GET https://workshop-server.azurewebsites.net/dequeue \ - -H "X-Service-Key: your-api-key" -``` - -**Example (PowerShell)**: -```powershell -$headers = @{ "X-Service-Key" = "your-api-key" } -Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/dequeue" ` - -Method Get ` - -Headers $headers -``` - -**Notes**: -- Documents are removed from the queue after dequeue (FIFO) -- Returns 404 if the queue is empty -- Once dequeued, the document cannot be retrieved again - ---- - -### 5. Clear Queue - -Clear all documents from your queue. - -**Endpoint**: `DELETE /clear` - -**Authentication**: Required (`X-Service-Key` header) - -**Request**: -```http -DELETE /clear HTTP/1.1 -X-Service-Key: your-api-key -``` - -**Response** (200 OK): -```json -{ - "status": "cleared" -} -``` - -**Example (cURL)**: -```bash -curl -X DELETE https://workshop-server.azurewebsites.net/clear \ - -H "X-Service-Key: your-api-key" -``` - -**Example (PowerShell)**: -```powershell -$headers = @{ "X-Service-Key" = "your-api-key" } -Invoke-RestMethod -Uri "https://workshop-server.azurewebsites.net/clear" ` - -Method Delete ` - -Headers $headers -``` - -**Notes**: -- Removes all documents from your queue -- Useful for testing and cleanup -- Cannot be undone - ---- - -## Error Responses - -All endpoints may return error responses in the following format: - -### 400 Bad Request -```json -{ - "detail": "Missing name" -} -``` -Returned when required parameters are missing or invalid. - -### 401 Unauthorized -```json -{ - "detail": "Unauthorized or invalid key" -} -``` -Returned when: -- The `X-Service-Key` header is missing -- The API key is invalid -- The API key doesn't match any registered user - -### 404 Not Found -```json -{ - "detail": "Queue empty" -} -``` -Returned when trying to dequeue from an empty queue. - ---- - -## Rate Limits - -Currently, there are **no rate limits** enforced. However, please be considerate of other workshop participants and: -- Don't spam the API with excessive requests -- Use reasonable document sizes (< 1 MB) -- Clean up your queue after testing - ---- - -## Data Persistence - -**Important**: -- All data is stored **in-memory only** -- If the server restarts, all queues and registrations are lost -- Don't rely on this API for production use -- This is a workshop server only - ---- - -## JSON Document Structure - -While you can send any valid JSON, the recommended structure for this workshop is: - -```json -{ - "documentType": "Invoice", // Type of document - "documentNo": "string", // Document number - "customerNo": "string", // Customer/Vendor number - "customerName": "string", // Customer/Vendor name - "postingDate": "YYYY-MM-DD", // ISO date format - "currencyCode": "string", // Currency (USD, EUR, etc.) - "totalAmount": 0.00, // Decimal number - "lines": [ // Array of line items - { - "lineNo": 0, // Integer line number - "type": "string", // Item, G/L Account, etc. - "no": "string", // Item/Account number - "description": "string", // Line description - "quantity": 0.00, // Decimal quantity - "unitPrice": 0.00, // Decimal unit price - "lineAmount": 0.00 // Decimal line amount - } - ] -} -``` - - -## Support - -If you encounter issues with the API: -1. Check your API key is correct -2. Verify the base URL is accessible -3. Check request headers and body format -4. Look at the error response details -5. Ask the instructor for help - ---- - -## Server Implementation - -The server is built with FastAPI (Python) and is extremely simple: -- In-memory storage (dictionary and deques) -- No database -- No authentication beyond the API key -- No encryption (workshop use only) - -See `server/server.py` for the complete source code. - ---- - -**Happy Testing!** πŸš€ diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/COMPLETE_WORKSHOP_GUIDE.md b/samples/EDocument/DirectionsEMEA2025/workshop/COMPLETE_WORKSHOP_GUIDE.md deleted file mode 100644 index 8d58babf..00000000 --- a/samples/EDocument/DirectionsEMEA2025/workshop/COMPLETE_WORKSHOP_GUIDE.md +++ /dev/null @@ -1,564 +0,0 @@ -# E-Document Connector Workshop - Complete Guide -## Directions EMEA 2025 - -Welcome! This comprehensive guide will take you through building a complete E-Document integration solution in **90 minutes**. - ---- - -## πŸ“‹ Table of Contents - -1. [Workshop Overview](#workshop-overview) -2. [Prerequisites & Setup](#prerequisites--setup) -3. [Understanding the Architecture](#understanding-the-architecture) -4. [Exercise 1: SimpleJson Format (30 min)](#exercise-1-simplejson-format-30-minutes) -5. [Exercise 2: DirectionsConnector (30 min)](#exercise-2-directionsconnector-30-minutes) -6. [Testing Complete Flow (15 min)](#testing-complete-flow-15-minutes) -7. [Troubleshooting](#troubleshooting) -8. [What's Next](#whats-next) - ---- - -## 🎯 Workshop Overview - -### What You'll Build - -By the end of this workshop, you will have created: - -1. **SimpleJson Format** - An E-Document format implementation that: - - Validates outgoing documents - - Converts Sales Invoices to JSON - - Parses incoming JSON documents - - Creates Purchase Invoices from received data - -2. **DirectionsConnector** - An HTTP API integration that: - - Sends documents to an Azure-hosted API - - Receives documents from the API queue - - Handles authentication and error responses - - Enables complete document round-trips - -### What You'll Learn - -- βœ… How the E-Document Core framework works -- βœ… How to implement the "E-Document" interface -- βœ… How to implement IDocumentSender and IDocumentReceiver interfaces -- βœ… Best practices for E-Document integrations -- βœ… Testing and debugging E-Document flows - -### Timeline - -| Time | Duration | Activity | -|------|----------|----------| -| 00:00-00:10 | 10 min | Introduction & Architecture Overview | -| 00:10-00:40 | 30 min | **Exercise 1**: SimpleJson Format | -| 00:40-01:10 | 30 min | **Exercise 2**: DirectionsConnector | -| 01:10-01:25 | 15 min | Testing & Live Demo | -| 01:25-01:30 | 5 min | Wrap-up & Q&A | - ---- - -## πŸ“¦ Prerequisites & Setup - -### Required - -- βœ… Business Central development environment (Sandbox or Docker) -- βœ… VS Code with AL Language extension -- βœ… Basic AL programming knowledge -- βœ… API Base URL (provided by instructor) - -### Workspace Structure - -Your workspace contains these folders: - -``` -application/ - β”œβ”€β”€ simple_json/ # Exercise 1: Format implementation - β”‚ β”œβ”€β”€ SimpleJsonFormat.Codeunit.al # ⚠️ TODO sections - β”‚ β”œβ”€β”€ SimpleJsonFormat.EnumExt.al # βœ… Pre-written - β”‚ β”œβ”€β”€ SimpleJsonHelper.Codeunit.al # βœ… Pre-written helpers - β”‚ └── app.json - β”‚ - └── directions_connector/ # Exercise 2: Integration implementation - β”œβ”€β”€ ConnectorIntegration.Codeunit.al # ⚠️ TODO sections - β”œβ”€β”€ ConnectorIntegration.EnumExt.al # βœ… Pre-written - β”œβ”€β”€ ConnectorAuth.Codeunit.al # βœ… Pre-written helpers - β”œβ”€β”€ ConnectorRequests.Codeunit.al # βœ… Pre-written helpers - β”œβ”€β”€ ConnectorConnectionSetup.Table.al # βœ… Pre-written - β”œβ”€β”€ ConnectorConnectionSetup.Page.al # βœ… Pre-written - └── app.json -``` - -### What's Pre-Written vs. What You'll Implement - -**Pre-written (to save time):** -- Extension setup and dependencies -- Enum extensions -- Helper methods for JSON and HTTP operations -- Authentication and connection setup -- UI pages and tables - -**You'll implement (core business logic):** -- Document validation -- JSON creation and parsing -- HTTP request handling -- Document sending and receiving - ---- - -## πŸ—οΈ Understanding the Architecture - -### E-Document Core Framework - -The framework provides a standardized way to integrate Business Central with external systems: - -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Business Central β”‚ -β”‚ β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚Sales Invoice │─────▢│ E-Document Core β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Format Interface │◀─── Exercise 1 β”‚ -β”‚ β”‚ (SimpleJson) β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ JSON Blob β”‚ -β”‚ β”‚ β”‚ -β”‚ β–Ό β”‚ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚Integration Interface│◀─── Exercise 2 β”‚ -β”‚ β”‚ (Connector) β”‚ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - β”‚ HTTPS - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ Azure API Server β”‚ - β”‚ Queue Management β”‚ - β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - -### Two Key Interfaces - -#### 1. Format Interface ("E-Document") -Converts documents between Business Central and external formats. - -**Outgoing (BC β†’ External):** -- `Check()` - Validate before sending -- `Create()` - Convert to format (JSON, XML, etc.) - -**Incoming (External β†’ BC):** -- `GetBasicInfoFromReceivedDocument()` - Parse metadata -- `GetCompleteInfoFromReceivedDocument()` - Create BC document - -#### 2. Integration Interface (IDocumentSender/IDocumentReceiver) -Handles communication with external systems. - -**Sending:** -- `Send()` - Send document to external service - -**Receiving:** -- `ReceiveDocuments()` - Get list of available documents -- `DownloadDocument()` - Download specific document - -### SimpleJson Format Structure - -```json -{ - "documentType": "Invoice", - "documentNo": "SI-001", - "customerNo": "C001", - "customerName": "Contoso Ltd.", - "postingDate": "2025-10-27", - "currencyCode": "USD", - "totalAmount": 1250.00, - "lines": [ - { - "lineNo": 10000, - "type": "Item", - "no": "ITEM-001", - "description": "Widget", - "quantity": 5, - "unitPrice": 250.00, - "lineAmount": 1250.00 - } - ] -} -``` - -### API Endpoints - -| Endpoint | Method | Purpose | -|----------|--------|---------| -| `/register` | POST | Get API key | -| `/enqueue` | POST | Send document | -| `/peek` | GET | View queue | -| `/dequeue` | GET | Receive & remove document | -| `/clear` | DELETE | Clear queue | - -**Authentication:** All endpoints (except `/register`) require: -``` -X-Service-Key: your-api-key-here -``` - ---- - -## πŸš€ Exercise 1: SimpleJson Format (30 minutes) - -### Overview - -You'll implement the **"E-Document" interface** to convert Business Central documents to/from JSON format. - -**File:** `application/simple_json/SimpleJsonFormat.Codeunit.al` - ---- - -### Part A: Validate Outgoing Documents (5 minutes) - -**Goal:** Ensure required fields are filled before creating the document. - -**Find:** The `Check()` procedure (around line 27) - -**Task:** Add validation for the Posting Date field. - -**Hint:** Use the same pattern as the Customer No. validation above, using `TestField()` on the Posting Date field. - ---- - -### Part B: Create JSON from Sales Invoice (15 minutes) - -**Goal:** Generate a JSON representation of a Sales Invoice with header and lines. - -**Find:** The `CreateSalesInvoiceJson()` procedure (around line 68) - -**Task:** Complete the TODOs to add: -- customerNo and customerName to the header -- description and quantity to lines - ---- - -### Part C: Parse Incoming JSON (Basic Info) (5 minutes) - -**Goal:** Extract basic information from incoming JSON to populate E-Document fields. - -**Find:** The `GetBasicInfoFromReceivedDocument()` procedure (around line 140) - -**Task:** Complete the TODOs to extract: -- Vendor number (from "customerNo" in JSON) -- Vendor name (from "customerName" in JSON) -- Total amount (from "totalAmount" in JSON) - - ---- - -### Part D: Create Purchase Invoice from JSON (5 minutes) - -**Goal:** Create a complete Purchase Invoice record from JSON data. - -**Find:** The `GetCompleteInfoFromReceivedDocument()` procedure (around line 178) - -**Task:** Complete all the TODO sections with '???' placeholders: -- Set vendor number from JSON (from "customerNo") -- Set posting date from JSON (from "postingDate") -- Set line description from JSON (from "description") -- Set line quantity from JSON (from "quantity") -- Set line unit cost from JSON (from "unitPrice") - ---- - -### βœ… Exercise 1 Complete! - -You've now implemented the complete E-Document format interface. Build and deploy your extension before moving to Exercise 2. - ---- - -## πŸ”Œ Exercise 2: DirectionsConnector (30 minutes) - -### Overview - -You'll implement the **IDocumentSender** and **IDocumentReceiver** interfaces to send/receive documents via HTTP API. - -**File:** `application/directions_connector/ConnectorIntegration.Codeunit.al` - ---- - -### Part A: Setup Connection (5 minutes) - -Before implementing code, you need to configure the connection in Business Central. - -**Steps:** - -1. **Open Business Central** in your browser - -2. **Search** for "Connector Connection Setup" - -3. **Enter Configuration:** - - **API Base URL**: `[Provided by instructor]` - - Example: `https://edocument-workshop.azurewebsites.net/` - - **User Name**: Your unique name (e.g., "john-smith") - -4. **Click "Register"** to get your API key - - The system will call the API and save your key automatically - -5. **Click "Test Connection"** to verify - - You should see "Connection test successful!" - -**βœ… Test:** You should now have a valid API key stored in the setup. - ---- - -### Part B: Send Document (10 minutes) - -**Goal:** Send an E-Document to the API `/enqueue` endpoint. - -**Find:** The `Send()` procedure (around line 31) - -**Task:** Complete the TODOs to: -- Get the temp blob with JSON from SendContext -- Create POST request to 'enqueue' endpoint -- Send the HTTP request and handle the response - -**Hints:** -- Use `SendContext.GetTempBlob()` to get the TempBlob -- Use `ConnectorRequests.ReadJsonFromBlob()` to read JSON text from blob -- Build the endpoint URL: `ConnectorSetup."API Base URL" + 'enqueue'` -- Use `ConnectorRequests.CreatePostRequest()` to create the request -- Use `ConnectorAuth.AddAuthHeader()` to add authentication -- Use `HttpClient.Send()` to send the request -- Use `SendContext.Http().SetHttpRequestMessage()` and `SetHttpResponseMessage()` to log -- Use `ConnectorRequests.CheckResponseSuccess()` to verify success - -**βœ… Test:** This will be tested when sending a Sales Invoice as an E-Document. - ---- - -### Part C: Receive Documents List (10 minutes) - -**Goal:** Retrieve the list of available documents from the API `/peek` endpoint. - -**Find:** The `ReceiveDocuments()` procedure (around line 72) - -**Task:** Complete the TODOs to: -- Create GET request to 'peek' endpoint -- Send the HTTP request and handle the response -- Add each document TempBlob to DocumentsMetadata list - -**Hints:** -- Build endpoint URL: `ConnectorSetup."API Base URL" + 'peek'` -- Use `ConnectorRequests.CreateGetRequest()` for GET requests -- Use `ConnectorAuth.AddAuthHeader()` to add authentication -- Use `HttpClient.Send()` to send the request -- Use `ReceiveContext.Http().Set...()` to log the request/response -- Parse response: `JsonObject.ReadFrom(ResponseText)` -- Get items array: `JsonObject.Get('items', JsonToken)` then `JsonToken.AsArray()` -- For each document in array, write to TempBlob and add to `DocumentsMetadata.Add(TempBlob)` - -**βœ… Test:** This will be tested when using "Get Documents" action in BC. - ---- - -### Part D: Download Single Document (5 minutes) - -**Goal:** Download a single document from the API `/dequeue` endpoint. - -**Find:** The `DownloadDocument()` procedure (around line 135) - -**Task:** Complete the TODOs to: -- Create GET request to 'dequeue' endpoint -- Send the HTTP request and handle the response - -**Hints:** -- Build endpoint URL: `ConnectorSetup."API Base URL" + 'dequeue'` -- Use `ConnectorRequests.CreateGetRequest()` for GET requests -- Use `ConnectorAuth.AddAuthHeader()` to add authentication -- Use `HttpClient.Send()` to send the request -- Use `ReceiveContext.Http().Set...()` to log -- Parse response: `JsonObject.ReadFrom(ResponseText)` -- Get document: `JsonObject.Get('document', JsonToken)` -- Store in TempBlob: `TempBlob := ReceiveContext.GetTempBlob()` then use `ConnectorRequests.WriteTextToBlob()` - -**βœ… Test:** This will be tested in the complete flow when receiving documents. - ---- - -### βœ… Exercise 2 Complete! - -You've now implemented the complete E-Document integration interface. Build and deploy your extension before testing. - ---- - -## πŸ§ͺ Testing Complete Flow (15 minutes) - -### Setup E-Document Service - -1. **Open Business Central** - -2. **Search** for "E-Document Services" - -3. **Create New Service:** - - **Code**: `CONNECTOR` - - **Description**: `Directions Connector Workshop` - - **Document Format**: `Simple JSON Format - Exercise 1` - - **Service Integration V2**: `Connector` - -4. **Click "Setup"** to open connection setup and verify configuration - -5. **Enable the service** by toggling the "Enabled" field - ---- - -### Test Outgoing Flow (Sending Documents) - -#### Step 1: Create Sales Invoice - -1. **Search** for "Sales Invoices" -2. **Create New** invoice: - - **Customer**: Any customer (e.g., "10000") - - **Posting Date**: Today -3. **Add Lines**: - - Type: Item - - No.: Any item (e.g., "1000") - - Quantity: 5 - - Unit Price: 250 -4. **Post** the invoice - -#### Step 2: Send E-Document - -1. **Search** for "E-Documents" -2. **Find** your posted invoice (Status: "Processed") -3. **Click "Send"** action -4. **Verify** status changes to "Sent" -5. **Open the E-Document** and check: - - "E-Document Log" shows the JSON content - - "Integration Log" shows the HTTP request/response - -#### Step 3: Verify in API - -You can verify the document reached the API: - -**Option 1 - Browser with extension:** -- Install a browser extension like "ModHeader" or "Modify Header Value" -- Add header: `X-Service-Key: [your-api-key]` -- Navigate to: `[API-Base-URL]/peek` -- You should see your document in the "items" array - -**Option 2 - PowerShell:** -```powershell -$headers = @{ "X-Service-Key" = "your-api-key-here" } -Invoke-RestMethod -Uri "https://[API-URL]/peek" -Headers $headers -``` - -**βœ… Success:** You should see your Sales Invoice data in JSON format in the queue! - ---- - -### Test Incoming Flow (Receiving Documents) - -#### Step 1: Ensure Documents in Queue - -Make sure there are documents in the API queue: -- Either use documents you sent earlier -- Or have a partner send documents to your queue -- Or the instructor can provide test documents - -#### Step 2: Receive Documents - -1. **Open** "E-Document Services" -2. **Select** your CONNECTOR service -3. **Click** "Get Documents" action -4. **Wait** for processing to complete - -#### Step 3: View Received E-Documents - -1. **Search** for "E-Documents" -2. **Filter** by Status: "Imported" -3. **Open** a received E-Document -4. **Verify**: - - Document No. is populated - - Vendor No. is populated - - Vendor Name is populated - - Document Date is set - - Amount is correct -5. **Check logs**: - - "E-Document Log" shows the received JSON - - "Integration Log" shows the HTTP request/response - -#### Step 4: Create Purchase Invoice - -1. **From the E-Document**, click "Create Document" -2. **Open** the created Purchase Invoice -3. **Verify**: - - Vendor is set correctly - - Posting Date matches - - Currency matches (if specified) - - Lines are populated with: - - Item numbers - - Descriptions - - Quantities - - Unit costs - - Line amounts - -#### Step 5: Verify Queue is Empty - -Check that documents were removed from the queue: -- Use the `/peek` endpoint again -- The queue should now be empty (or have fewer documents) - -**βœ… Success:** You've completed a full document round-trip! - ---- - -## πŸ› Troubleshooting - - -### Additional Resources - -**E-Document Framework:** -- [E-Document Core Documentation](https://github.com/microsoft/BCApps/blob/main/src/Apps/W1/EDocument/App/README.md) -- [E-Document Interface Source](https://github.com/microsoft/BCApps/blob/main/src/Apps/W1/EDocument/App/src/Document/Interfaces/EDocument.Interface.al) -- [Integration Interfaces Source](https://github.com/microsoft/BCApps/tree/main/src/Apps/W1/EDocument/App/src/Integration/Interfaces) - -**Business Central:** -- [BC Developer Documentation](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/) -- [AL Language Reference](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-reference-overview) -- [AL Samples Repository](https://github.com/microsoft/AL) - -**Workshop Materials:** -- [API Reference](./API_REFERENCE.md) - Complete API documentation -- [Server README](../server/README.md) - API server details - ---- - -## πŸŽ‰ Congratulations! - -You've successfully completed the E-Document Connector Workshop! - -### What You've Accomplished - -- βœ… Implemented a complete E-Document format (SimpleJson) -- βœ… Implemented a complete E-Document integration (DirectionsConnector) -- βœ… Sent documents from Business Central to an external API -- βœ… Received documents from an external API into Business Central -- βœ… Converted between BC documents and JSON format -- βœ… Understood the E-Document framework architecture -- βœ… Gained hands-on experience with real-world integration patterns - -### Skills Gained - -- Understanding of the E-Document Core framework -- Experience implementing format interfaces -- Experience implementing integration interfaces -- HTTP API integration best practices -- JSON data mapping and transformation -- Testing and debugging E-Document flows - ---- - -**Thank you for participating!** We hope you found this workshop valuable and are excited to build E-Document integrations in your projects. - -**Happy Coding!** πŸš€ - ---- diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_GUIDE.md b/samples/EDocument/DirectionsEMEA2025/workshop/WORKSHOP_GUIDE.md new file mode 100644 index 00000000..e69de29b diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/docprofile.png b/samples/EDocument/DirectionsEMEA2025/workshop/docprofile.png new file mode 100644 index 0000000000000000000000000000000000000000..4b66d8ae7ff63ffcd757d28e85fa5b9699d95917 GIT binary patch literal 165435 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 literal 0 HcmV?d00001 diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/service.png b/samples/EDocument/DirectionsEMEA2025/workshop/service.png new file mode 100644 index 0000000000000000000000000000000000000000..76ca79f80d17a7f7b1f7eda6de355d2e3ed48615 GIT binary patch literal 65117 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%0AE( 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 X8sRY4=i5;EZS|^(nz!>6?mqo*mTLyF literal 0 HcmV?d00001 From 62bd1a4716b5fee2cae7b868be83babcb34d0de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Hartvig=20Gr=C3=B8nbech?= Date: Wed, 5 Nov 2025 13:13:16 +0100 Subject: [PATCH 9/9] Fixes --- .../DirectionsEMEA2025.code-workspace | 8 -------- .../ConnectorConnectionSetup.Page.al | 2 -- .../ConnectorConnectionSetup.Table.al | 2 +- .../ConnectorIntegration.Codeunit.al | 4 ++-- .../DirectionsEMEA2025/workshop/image.png | Bin 0 -> 72563 bytes 5 files changed, 3 insertions(+), 13 deletions(-) create mode 100644 samples/EDocument/DirectionsEMEA2025/workshop/image.png diff --git a/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace b/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace index c91753a5..538ad65a 100644 --- a/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace +++ b/samples/EDocument/DirectionsEMEA2025/DirectionsEMEA2025.code-workspace @@ -12,14 +12,6 @@ "name": "Server", "path": "./server" }, - { - "name": "E-Document Core", - "path": "C:\\depot\\NAV_1\\App\\BCApps\\src\\Apps\\W1\\EDocument\\App" - }, - { - "name": "Avalara Connector", - "path": "C:\\depot\\NAV_1\\App\\BCApps\\src\\Apps\\W1\\EDocumentConnectors\\Avalara\\App" - }, { "name": "Workshop", "path": "workshop" diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Page.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Page.al index 5266ee70..aa002144 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Page.al +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Page.al @@ -50,8 +50,6 @@ page 50122 "Connector Connection Setup" ApplicationArea = All; Caption = 'API Key'; ToolTip = 'Specifies the API key received after registration'; - Editable = false; - ExtendedDatatype = Masked; trigger OnAssistEdit() var diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Table.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Table.al index 7f4136e7..36953bb7 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Table.al +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorConnectionSetup.Table.al @@ -89,6 +89,6 @@ table 50122 "Connector Connection Setup" ///

procedure GetAPIKeyText(): Text begin - exit(Format("API Key").Replace('{', '').Replace('}', '')); + exit(Format("API Key").Replace('{', '').Replace('}', '').ToLower()); end; } diff --git a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.Codeunit.al b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.Codeunit.al index 1bdf645e..1fb5c48f 100644 --- a/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.Codeunit.al +++ b/samples/EDocument/DirectionsEMEA2025/application/directions_connector/ConnectorIntegration.Codeunit.al @@ -60,7 +60,7 @@ codeunit 50123 "Connector Integration" implements IDocumentSender, IDocumentRece ConnectorAuth.AddAuthHeader(HttpRequest, ConnectorSetup); // TODO: Send the HTTP request and handle the response using HttpClient - + if not HttpClient.Send(HttpRequest, HttpResponse) then Error('Failed to connect to the API server.'); // @@ -110,7 +110,7 @@ codeunit 50123 "Connector Integration" implements IDocumentSender, IDocumentRece // TODO: Send the HTTP request and handle the response using HttpClient - + if not HttpClient.Send(HttpRequest, HttpResponse) then Error('Failed to connect to the API server.'); // diff --git a/samples/EDocument/DirectionsEMEA2025/workshop/image.png b/samples/EDocument/DirectionsEMEA2025/workshop/image.png new file mode 100644 index 0000000000000000000000000000000000000000..3fbcaf88a00b0f5b38a2ee3d1cd1433dfab159d4 GIT binary patch literal 72563 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 IbMmME0%P