add webhook to docseal
This commit is contained in:
69
enrollments-events/app/app.py
Normal file
69
enrollments-events/app/app.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from aws_lambda_powertools import Logger, Tracer
|
||||
from aws_lambda_powertools.event_handler.api_gateway import (
|
||||
APIGatewayHttpResolver,
|
||||
Response,
|
||||
)
|
||||
from aws_lambda_powertools.logging import correlation_paths
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||
|
||||
from boto3clients import dynamodb_client, s3_client
|
||||
from config import BUCKET_NAME, ENROLLMENT_TABLE
|
||||
from docseal import headers
|
||||
|
||||
logger = Logger(__name__)
|
||||
tracer = Tracer()
|
||||
app = APIGatewayHttpResolver(enable_validation=True)
|
||||
dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||
|
||||
|
||||
@app.post('/')
|
||||
@tracer.capture_method
|
||||
def postback():
|
||||
json_body = app.current_event.json_body
|
||||
enrollment_id = json_body['data']['name']
|
||||
combined_document_url = json_body['data'].get('combined_document_url')
|
||||
|
||||
if not combined_document_url:
|
||||
return Response(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
try:
|
||||
r = requests.get(combined_document_url, headers=headers)
|
||||
r.raise_for_status()
|
||||
|
||||
object_key = f'certs/{enrollment_id}.pdf'
|
||||
s3_uri = f's3://{BUCKET_NAME}/{object_key}'
|
||||
|
||||
s3_client.put_object(
|
||||
Bucket=BUCKET_NAME,
|
||||
Key=object_key,
|
||||
Body=r.content,
|
||||
ContentType='application/pdf',
|
||||
)
|
||||
|
||||
logger.debug(f'PDF uploaded successfully to {s3_uri}')
|
||||
except requests.exceptions.RequestException as exc:
|
||||
logger.exception(exc)
|
||||
raise
|
||||
|
||||
dyn.update_item(
|
||||
key=KeyPair(enrollment_id, '0'),
|
||||
cond_expr='attribute_exists(sk)',
|
||||
update_expr='SET cert.s3_uri = :s3_uri',
|
||||
expr_attr_values={':s3_uri': s3_uri},
|
||||
)
|
||||
|
||||
return Response(status_code=HTTPStatus.NO_CONTENT)
|
||||
|
||||
|
||||
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP)
|
||||
@tracer.capture_lambda_handler
|
||||
def lambda_handler(
|
||||
event: dict[str, Any],
|
||||
context: LambdaContext,
|
||||
) -> dict[str, Any]:
|
||||
return app.resolve(event, context)
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import TypedDict
|
||||
from typing import NotRequired, TypedDict
|
||||
|
||||
import requests
|
||||
|
||||
@@ -8,8 +8,22 @@ headers = {
|
||||
'X-Auth-Token': DOCSEAL_KEY,
|
||||
}
|
||||
|
||||
Submitter = TypedDict('Submitter', {'role': str, 'name': str, 'email': str})
|
||||
EmailMessage = TypedDict('EmailMessage', {'subject': str, 'body': str})
|
||||
Submitter = TypedDict(
|
||||
'Submitter',
|
||||
{
|
||||
'role': str,
|
||||
'name': str,
|
||||
'email': str,
|
||||
'external_id': NotRequired[str],
|
||||
},
|
||||
)
|
||||
EmailMessage = TypedDict(
|
||||
'EmailMessage',
|
||||
{
|
||||
'subject': str,
|
||||
'body': str,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def create_submission_from_pdf(
|
||||
|
||||
@@ -45,9 +45,10 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
|
||||
},
|
||||
submitters=[
|
||||
{
|
||||
'role': 'Aluno',
|
||||
'role': 'First Party',
|
||||
'name': user['name'],
|
||||
'email': user['email'],
|
||||
'external_id': user['id'],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
@@ -50,6 +50,39 @@ Resources:
|
||||
Properties:
|
||||
RetentionInDays: 90
|
||||
|
||||
HttpLog:
|
||||
Type: AWS::Logs::LogGroup
|
||||
Properties:
|
||||
RetentionInDays: 90
|
||||
|
||||
HttpApi:
|
||||
Type: AWS::Serverless::HttpApi
|
||||
Properties:
|
||||
CorsConfiguration:
|
||||
AllowOrigins: ["*"]
|
||||
AllowMethods: [POST, OPTIONS]
|
||||
AllowHeaders: [Content-Type, X-Requested-With]
|
||||
|
||||
HttpApiFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
Handler: app.lambda_handler
|
||||
Timeout: 12
|
||||
LoggingConfig:
|
||||
LogGroup: !Ref HttpLog
|
||||
Policies:
|
||||
- DynamoDBWritePolicy:
|
||||
TableName: !Ref EnrollmentTable
|
||||
- S3WritePolicy:
|
||||
BucketName: !Ref BucketName
|
||||
Events:
|
||||
Post:
|
||||
Type: HttpApi
|
||||
Properties:
|
||||
Path: /
|
||||
Method: POST
|
||||
ApiId: !Ref HttpApi
|
||||
|
||||
EventSetSubscriptionCoveredFunction:
|
||||
Type: AWS::Serverless::Function
|
||||
Properties:
|
||||
@@ -422,3 +455,13 @@ Resources:
|
||||
- prefix: CERT_REPORTING#ORG
|
||||
sk:
|
||||
- suffix: SCHEDULE#SEND_REPORT_EMAIL
|
||||
|
||||
Outputs:
|
||||
HttpApiUrl:
|
||||
Description: URL of your API endpoint
|
||||
Value:
|
||||
Fn::Sub: "https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}"
|
||||
HttpApiId:
|
||||
Description: Api ID of HttpApi
|
||||
Value:
|
||||
Ref: HttpApi
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from http import HTTPMethod
|
||||
|
||||
import jsonlines
|
||||
import pytest
|
||||
@@ -17,6 +20,7 @@ def pytest_configure():
|
||||
os.environ['DOCSEAL_KEY'] = 'gUWhWtYBgTaP8fc1q5GZ6JuUHaZzMgZna6KFBHz3Gzk'
|
||||
os.environ['USER_TABLE'] = PYTEST_TABLE_NAME
|
||||
os.environ['COURSE_TABLE'] = PYTEST_TABLE_NAME
|
||||
os.environ['ENROLLMENT_TABLE'] = PYTEST_TABLE_NAME
|
||||
os.environ['ORDER_TABLE'] = PYTEST_TABLE_NAME
|
||||
os.environ['ENROLLMENT_TABLE'] = PYTEST_TABLE_NAME
|
||||
os.environ['BUCKET_NAME'] = 'saladeaula.digital'
|
||||
@@ -36,6 +40,78 @@ def lambda_context() -> LambdaContext:
|
||||
return LambdaContext()
|
||||
|
||||
|
||||
class HttpApiProxy:
|
||||
def __call__(
|
||||
self,
|
||||
raw_path: str,
|
||||
method: str = HTTPMethod.GET,
|
||||
body: dict = {},
|
||||
*,
|
||||
headers: dict = {},
|
||||
auth_flow_type: str = 'USER_AUTH',
|
||||
queryStringParameters: dict = {},
|
||||
**kwargs,
|
||||
) -> dict:
|
||||
return {
|
||||
'version': '2.0',
|
||||
'routeKey': '$default',
|
||||
'rawPath': raw_path,
|
||||
'rawQueryString': 'parameter1=value1¶meter1=value2¶meter2=value',
|
||||
'cookies': ['cookie1', 'cookie2'],
|
||||
'headers': headers,
|
||||
'queryStringParameters': queryStringParameters,
|
||||
'requestContext': {
|
||||
'accountId': '123456789012',
|
||||
'apiId': 'api-id',
|
||||
'authorizer': {
|
||||
'lambda': {
|
||||
'user': {
|
||||
'name': 'Sérgio R Siqueira',
|
||||
'email': 'sergio@somosbeta.com.br',
|
||||
'email_verified': 'true',
|
||||
'custom:user_id': '5OxmMjL-ujoR5IMGegQz',
|
||||
'sub': 'c4f30dbd-083e-4b84-aa50-c31afe9b9c01',
|
||||
},
|
||||
'auth_flow_type': auth_flow_type,
|
||||
},
|
||||
'jwt': {
|
||||
'claims': {'claim1': 'value1', 'claim2': 'value2'},
|
||||
'scopes': ['scope1', 'scope2'],
|
||||
},
|
||||
},
|
||||
'domainName': 'id.execute-api.us-east-1.amazonaws.com',
|
||||
'domainPrefix': 'id',
|
||||
'http': {
|
||||
'method': str(method),
|
||||
'path': raw_path,
|
||||
'protocol': 'HTTP/1.1',
|
||||
'sourceIp': '192.168.0.1/32',
|
||||
'userAgent': 'agent',
|
||||
},
|
||||
'requestId': 'id',
|
||||
'routeKey': '$default',
|
||||
'stage': '$default',
|
||||
'time': '12/Mar/2020:19:03:58 +0000',
|
||||
'timeEpoch': 1583348638390,
|
||||
},
|
||||
'body': _base64_dict(body),
|
||||
'pathParameters': {'parameter1': 'value1'},
|
||||
'isBase64Encoded': True,
|
||||
'stageVariables': {'stageVariable1': 'value1', 'stageVariable2': 'value2'},
|
||||
}
|
||||
|
||||
|
||||
def _base64_dict(obj: dict = {}) -> str | None:
|
||||
if not obj:
|
||||
return None
|
||||
return base64.b64encode(json.dumps(obj).encode()).decode()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def http_api_proxy():
|
||||
return HttpApiProxy()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dynamodb_client():
|
||||
from boto3clients import dynamodb_client as client
|
||||
@@ -80,3 +156,10 @@ def seeds(dynamodb_client):
|
||||
TableName=PYTEST_TABLE_NAME,
|
||||
Item=serialize(line),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
import app
|
||||
|
||||
return app
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import app.events.send_reminder_emails as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||
|
||||
import events.send_reminder_emails as app
|
||||
|
||||
|
||||
def test_reminder_access_period_before_30_days(
|
||||
seeds,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import app.events.send_reminder_emails as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
|
||||
import events.send_reminder_emails as app
|
||||
|
||||
|
||||
def test_reminder_cert_expiration_before_30_days(
|
||||
seeds,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import app.events.send_reminder_emails as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
|
||||
import events.send_reminder_emails as app
|
||||
|
||||
|
||||
def test_reminder_no_access_after_3_days(
|
||||
seeds,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import app.events.send_reminder_emails as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
|
||||
import events.send_reminder_emails as app
|
||||
|
||||
|
||||
def test_reminder_no_activity_after_7_days(
|
||||
seeds,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from datetime import timedelta
|
||||
|
||||
import app.events.reporting.append_cert as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dateutils import now
|
||||
from layercake.dynamodb import (
|
||||
@@ -9,6 +8,8 @@ from layercake.dynamodb import (
|
||||
TransactKey,
|
||||
)
|
||||
|
||||
import events.reporting.append_cert as app
|
||||
|
||||
|
||||
def test_append_cert(
|
||||
seeds,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import app.events.reporting.send_report_email as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import (
|
||||
DynamoDBPersistenceLayer,
|
||||
KeyPair,
|
||||
)
|
||||
|
||||
import events.reporting.send_report_email as app
|
||||
|
||||
|
||||
def test_send_report_email(
|
||||
monkeypatch,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import app.events.stopgap.patch_course_metadata as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import (
|
||||
DynamoDBPersistenceLayer,
|
||||
SortKey,
|
||||
TransactKey,
|
||||
KeyPair,
|
||||
)
|
||||
|
||||
import events.stopgap.patch_course_metadata as app
|
||||
|
||||
|
||||
def test_enroll(
|
||||
seeds,
|
||||
@@ -27,10 +27,8 @@ def test_enroll(
|
||||
}
|
||||
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||
|
||||
result = dynamodb_persistence_layer.collection.get_items(
|
||||
TransactKey('47ZxxcVBjvhDS5TE98tpfQ')
|
||||
+ SortKey('0')
|
||||
+ SortKey('METADATA#DEDUPLICATION_WINDOW')
|
||||
r = dynamodb_persistence_layer.collection.get_item(
|
||||
KeyPair('47ZxxcVBjvhDS5TE98tpfQ', '0')
|
||||
)
|
||||
|
||||
assert 'METADATA#DEDUPLICATION_WINDOW' in result
|
||||
assert 'access_expires_at' in r
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import app.events.allocate_slots as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
|
||||
|
||||
import events.allocate_slots as app
|
||||
|
||||
|
||||
def test_allocate_slots(
|
||||
seeds,
|
||||
|
||||
@@ -14,6 +14,7 @@ def test_ask_to_sign(
|
||||
's3_uri': 's3://saladeaula.digital/certs/samples/nr11-operador-de-munck.pdf'
|
||||
},
|
||||
'user': {
|
||||
'id': '5OxmMjL-ujoR5IMGegQz',
|
||||
'name': 'Sérgio R Siqueira',
|
||||
'email': 'sergio@somosbeta.com.br',
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import app.events.enroll as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, PartitionKey
|
||||
|
||||
import events.enroll as app
|
||||
|
||||
|
||||
def test_enroll(
|
||||
seeds,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import app.events.reenroll_if_failed as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||
|
||||
import events.reenroll_if_failed as app
|
||||
|
||||
|
||||
def test_reenroll_custom_dedup_window(
|
||||
seeds,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import app.events.schedule_reminders as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import (
|
||||
DynamoDBPersistenceLayer,
|
||||
PartitionKey,
|
||||
)
|
||||
|
||||
import events.schedule_reminders as app
|
||||
|
||||
|
||||
def test_schedule_reminders(
|
||||
seeds,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import app.events.set_access_expired as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import (
|
||||
DynamoDBPersistenceLayer,
|
||||
@@ -6,6 +5,8 @@ from layercake.dynamodb import (
|
||||
TransactKey,
|
||||
)
|
||||
|
||||
import events.set_access_expired as app
|
||||
|
||||
|
||||
def test_set_access_expired(
|
||||
seeds,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import app.events.set_cert_expired as app
|
||||
from aws_lambda_powertools.utilities.typing import LambdaContext
|
||||
from layercake.dynamodb import (
|
||||
DynamoDBPersistenceLayer,
|
||||
@@ -6,6 +5,8 @@ from layercake.dynamodb import (
|
||||
TransactKey,
|
||||
)
|
||||
|
||||
import events.set_cert_expired as app
|
||||
|
||||
|
||||
def test_set_cert_expired(
|
||||
seeds,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
// Enrollment
|
||||
{"id": "6437a282-6fe8-4e4d-9eb0-da1007238007", "sk": "0", "status": "IN_PROGRESS", "progress": 10}
|
||||
{"id": "845fe390-e3c3-4514-97f8-c42de0566cf0", "sk": "0", "status": "COMPLETED", "progress": 100}
|
||||
{"id": "845fe390-e3c3-4514-97f8-c42de0566cf0", "sk": "0", "status": "COMPLETED", "progress": 100, "cert": {"s3_uri": "s3://saladeaula.digital/certs/samples/nr11-operador-de-munck.pdf", "issued_at": "2025-08-24T01:44:42.703012-03:06"}}
|
||||
|
||||
{"id": "1ee108ae-67d4-4545-bf6d-4e641cdaa4e0", "sk": "0", "status": "COMPLETED", "score": 100, "course": {"id": "123", "name": "CIPA Grau de Risco 1"}, "user": {"name": "Kurt Cobain"}, "cert": {"s3_uri": "s3://saladeaula.digital/issuedcerts/1ee108ae-67d4-4545-bf6d-4e641cdaa4e0.pdf"}}
|
||||
{"id": "1ee108ae-67d4-4545-bf6d-4e641cdaa4e0", "sk": "STARTED", "started_at": "2025-08-24T01:44:42.703012-03:06"}
|
||||
|
||||
38
enrollments-events/tests/test_app.py
Normal file
38
enrollments-events/tests/test_app.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from http import HTTPMethod, HTTPStatus
|
||||
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||
|
||||
from .conftest import HttpApiProxy, LambdaContext
|
||||
|
||||
enrollment_id = '845fe390-e3c3-4514-97f8-c42de0566cf0'
|
||||
body = {
|
||||
'data': {
|
||||
'name': enrollment_id,
|
||||
'slug': 'jzNV3ZrF6T16tX',
|
||||
'combined_document_url': 'https://docs.eduseg.com.br/file/WyI3MGVhNGQwYS02YTE5LTQyYzItODgyNy1iZTc0Zjc2ZTlkNmUiLCJibG9iIiwxNzYyMjEwMjg2XQ--72caad853fdc1c64c5321368c7d883828be0b859ed39689355bac9dffe71b8e2/e249c51b-3e68-42eb-bb4b-20659263ce1c.pdf',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_postback(
|
||||
app,
|
||||
seeds,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
http_api_proxy: HttpApiProxy,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
# This data was added from seeds
|
||||
r = app.lambda_handler(
|
||||
http_api_proxy(
|
||||
raw_path='/',
|
||||
method=HTTPMethod.POST,
|
||||
body=body,
|
||||
),
|
||||
lambda_context,
|
||||
)
|
||||
assert r['statusCode'] == HTTPStatus.NO_CONTENT
|
||||
|
||||
r = dynamodb_persistence_layer.get_item(
|
||||
KeyPair('845fe390-e3c3-4514-97f8-c42de0566cf0', '0')
|
||||
)
|
||||
print(r)
|
||||
@@ -1,6 +1,6 @@
|
||||
import base64
|
||||
import uuid
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
from docseal import create_submission_from_pdf
|
||||
|
||||
@@ -13,10 +13,12 @@ Seu certificado já está pronto e aguardando apenas a sua assinatura digital.
|
||||
"""
|
||||
|
||||
|
||||
def test_create_submission_from_pdf():
|
||||
r = MagicMock()
|
||||
def Response(*args, **kwargs):
|
||||
return type('Response', (), {'raise_for_status': object})
|
||||
|
||||
with patch('docseal.requests.post', r):
|
||||
|
||||
def test_create_submission_from_pdf():
|
||||
with patch('docseal.requests.post', Response):
|
||||
with open('tests/sample.pdf', 'rb') as f:
|
||||
file = base64.b64encode(f.read())
|
||||
create_submission_from_pdf(
|
||||
|
||||
Reference in New Issue
Block a user