Skip to content

Commit 67702b0

Browse files
committed
MPT-8098 Provision FinOps order, create employee and organization
1 parent b147698 commit 67702b0

File tree

24 files changed

+937
-424
lines changed

24 files changed

+937
-424
lines changed

ffc/client.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import logging
2+
from datetime import datetime, timedelta, timezone
3+
from functools import wraps
4+
from urllib.parse import urljoin
5+
from uuid import uuid4
6+
7+
import jwt
8+
import requests
9+
from requests import HTTPError
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class FinOpsError(Exception):
15+
pass
16+
17+
18+
class FinOpsHttpError(FinOpsError):
19+
def __init__(self, status_code: int, content: str):
20+
self.status_code = status_code
21+
self.content = content
22+
super().__init__(f"{self.status_code} - {self.content}")
23+
24+
25+
class FinOpsNotFoundError(FinOpsHttpError):
26+
def __init__(self, content):
27+
super().__init__(404, content)
28+
29+
30+
def wrap_http_error(func):
31+
@wraps(func)
32+
def _wrapper(*args, **kwargs):
33+
try:
34+
return func(*args, **kwargs)
35+
except HTTPError as e:
36+
if e.response.status_code == 404:
37+
raise FinOpsNotFoundError(e.response.json())
38+
else:
39+
raise FinOpsHttpError(e.response.status_code, e.response.json())
40+
41+
return _wrapper
42+
43+
44+
class FinOpsClient:
45+
def __init__(self, base_url, sub, secret):
46+
self._sub = sub
47+
self._secret = secret
48+
self._api_base_url = base_url
49+
50+
self._jwt = None
51+
52+
@wrap_http_error
53+
def get_employee(self, email):
54+
headers = self._get_headers()
55+
response = requests.get(
56+
urljoin(self._api_base_url, f"/ops/v1/employees/{email}"),
57+
headers=headers,
58+
)
59+
60+
response.raise_for_status()
61+
62+
return response.json()
63+
64+
@wrap_http_error
65+
def create_employee(self, email, name):
66+
headers = self._get_headers()
67+
68+
response = requests.post(
69+
urljoin(self._api_base_url, "/ops/v1/employees"),
70+
headers=headers,
71+
json={
72+
"email": email,
73+
"display_name": name,
74+
},
75+
)
76+
77+
response.raise_for_status()
78+
79+
return response.json()
80+
81+
@wrap_http_error
82+
def create_organization(
83+
self,
84+
name,
85+
currency,
86+
billing_currency,
87+
external_id,
88+
user_id,
89+
):
90+
headers = self._get_headers()
91+
92+
response = requests.post(
93+
urljoin(self._api_base_url, "/ops/v1/organizations"),
94+
headers=headers,
95+
json={
96+
"name": name,
97+
"currency": currency,
98+
"billing_currency": billing_currency,
99+
"operations_external_id": external_id,
100+
"user_id": user_id,
101+
},
102+
)
103+
104+
response.raise_for_status()
105+
106+
return response.json()
107+
108+
def _get_headers(self):
109+
return {
110+
"Authorization": f"Bearer {self._get_auth_token()}",
111+
"Accept": "application/json",
112+
"Content-Type": "application/json",
113+
"X-Request-Id": str(uuid4()),
114+
}
115+
116+
def _get_auth_token(self):
117+
if not self._jwt or self._is_token_expired():
118+
now = datetime.now(tz=timezone.utc)
119+
self._jwt = jwt.encode(
120+
{
121+
"sub": self._sub,
122+
"exp": now + timedelta(minutes=5),
123+
"nbf": now,
124+
"iat": now,
125+
},
126+
self._secret,
127+
algorithm="HS256",
128+
)
129+
130+
return self._jwt
131+
132+
def _is_token_expired(self):
133+
try:
134+
jwt.decode(self._jwt, self._secret, algorithms=["HS256"])
135+
return False
136+
except jwt.ExpiredSignatureError:
137+
return True
138+
139+
140+
_FFC_CLIENT = None
141+
142+
143+
def get_ffc_client():
144+
"""
145+
Returns an instance of the `FinOpsClient`.
146+
147+
Returns:
148+
FinOpsClient: An instance of the `FinOpsClient`.
149+
"""
150+
from django.conf import settings
151+
152+
global _FFC_CLIENT
153+
if not _FFC_CLIENT:
154+
_FFC_CLIENT = FinOpsClient(
155+
settings.EXTENSION_CONFIG["FFC_OPERATIONS_API_BASE_URL"],
156+
settings.EXTENSION_CONFIG["FFC_SUB"],
157+
settings.EXTENSION_CONFIG["FFC_OPERATIONS_SECRET"],
158+
)
159+
return _FFC_CLIENT

ffc/flows/error.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,20 @@
55

66
def strip_trace_id(traceback):
77
return TRACE_ID_REGEX.sub("(<omitted>)", traceback)
8+
9+
10+
class ValidationError:
11+
def __init__(self, id, message):
12+
self.id = id
13+
self.message = message
14+
15+
def to_dict(self, **kwargs):
16+
return {
17+
"id": self.id,
18+
"message": self.message.format(**kwargs),
19+
}
20+
21+
22+
ERR_ORGANIZATION_NAME = ValidationError("FFC0001", "Organization name is required")
23+
ERR_CURRENCY = ValidationError("FFC0002", "Currency is required")
24+
ERR_ADMIN_CONTACT = ValidationError("FFC0003", "Administrator contact is required")

ffc/flows/fulfillment.py

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@
66
from ffc.flows.error import strip_trace_id
77
from ffc.flows.order import (
88
OrderContext,
9-
is_change_order,
109
is_purchase_order,
11-
is_termination_order,
1210
)
1311
from ffc.flows.steps import (
1412
CheckDueDate,
13+
CheckOrderParameters,
1514
CompleteOrder,
15+
CreateEmployee,
16+
CreateOrganization,
1617
CreateSubscription,
1718
ResetDueDate,
19+
SetupAgreementExternalId,
1820
SetupDueDate,
1921
)
2022
from ffc.notifications import notify_unhandled_exception_in_teams
@@ -25,21 +27,11 @@
2527
purchase = Pipeline(
2628
SetupDueDate(),
2729
CheckDueDate(),
30+
CheckOrderParameters(),
31+
CreateEmployee(),
32+
CreateOrganization(),
2833
CreateSubscription(),
29-
ResetDueDate(),
30-
CompleteOrder("purchase_order"),
31-
)
32-
33-
change_order = Pipeline(
34-
SetupDueDate(),
35-
CheckDueDate(),
36-
ResetDueDate(),
37-
CompleteOrder("purchase_order"),
38-
)
39-
40-
terminate = Pipeline(
41-
SetupDueDate(),
42-
CheckDueDate(),
34+
SetupAgreementExternalId(),
4335
ResetDueDate(),
4436
CompleteOrder("purchase_order"),
4537
)
@@ -62,10 +54,6 @@ def fulfill_order(client, order):
6254
try:
6355
if is_purchase_order(order):
6456
purchase.run(client, context)
65-
elif is_change_order(order):
66-
change_order.run(client, context)
67-
elif is_termination_order(order): # pragma: no branch
68-
terminate.run(client, context)
6957
except Exception:
7058
notify_unhandled_exception_in_teams(
7159
"fulfillment",

ffc/flows/fulfillment/__init__.py

Lines changed: 0 additions & 48 deletions
This file was deleted.

ffc/flows/fulfillment/pipelines.py

Lines changed: 0 additions & 31 deletions
This file was deleted.

ffc/flows/order.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from dataclasses import dataclass
1+
from dataclasses import dataclass, field
22

33
from swo.mpt.extensions.flows.context import Context as BaseContext
44

@@ -9,8 +9,6 @@
99
MPT_ORDER_STATUS_COMPLETED = "Completed"
1010

1111
ORDER_TYPE_PURCHASE = "Purchase"
12-
ORDER_TYPE_CHANGE = "Change"
13-
ORDER_TYPE_TERMINATION = "Termination"
1412

1513

1614
def is_purchase_order(order):
@@ -25,17 +23,11 @@ def is_purchase_order(order):
2523
return order["type"] == ORDER_TYPE_PURCHASE
2624

2725

28-
def is_change_order(order):
29-
return order["type"] == ORDER_TYPE_CHANGE
30-
31-
32-
def is_termination_order(order):
33-
return order["type"] == ORDER_TYPE_TERMINATION
34-
35-
3626
@dataclass
3727
class OrderContext(BaseContext):
3828
order: dict
29+
employee: dict = field(init=False, default=None)
30+
organization: dict = field(init=False, default=None)
3931

4032
def __str__(self):
4133
return f"{(self.type or '-').upper()} {self.order['id']}"

ffc/flows/steps/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
from ffc.flows.steps.complete_order import CompleteOrder
2-
from ffc.flows.steps.create_subscription import CreateSubscription
31
from ffc.flows.steps.due_date import CheckDueDate, ResetDueDate, SetupDueDate
2+
from ffc.flows.steps.finops import CreateEmployee, CreateOrganization
3+
from ffc.flows.steps.order import (
4+
CheckOrderParameters,
5+
CompleteOrder,
6+
SetupAgreementExternalId,
7+
)
8+
from ffc.flows.steps.subscription import CreateSubscription
49

510
__all__ = [
611
"CompleteOrder",
712
"CreateSubscription",
813
"CheckDueDate",
914
"ResetDueDate",
1015
"SetupDueDate",
16+
"CreateEmployee",
17+
"CreateOrganization",
18+
"CheckOrderParameters",
19+
"SetupAgreementExternalId",
1120
]

0 commit comments

Comments
 (0)