Merge pull request #1439 from dod-ccpo/171364906-billing-account-name-config
Billing account name config and refactor portfolio to_dictionary method
This commit is contained in:
commit
f8dd072340
@ -219,6 +219,7 @@ To generate coverage reports for the Javascript tests:
|
||||
|
||||
- `ASSETS_URL`: URL to host which serves static assets (such as a CDN).
|
||||
- `AZURE_ACCOUNT_NAME`: The name for the Azure blob storage account
|
||||
- `AZURE_BILLING_ACCOUNT_NAME`: The name for the root Azure billing account
|
||||
- `AZURE_CALC_CLIENT_ID`: The client id used to generate a token for the Azure pricing calculator
|
||||
- `AZURE_CALC_RESOURCE`: The resource URL used to generate a token for the Azure pricing calculator
|
||||
- `AZURE_CALC_SECRET`: The secret key used to generate a token for the Azure pricing calculator
|
||||
|
11
atst/jobs.py
11
atst/jobs.py
@ -209,10 +209,17 @@ def send_PPOC_email(portfolio_dict):
|
||||
)
|
||||
|
||||
|
||||
def make_initial_csp_data(portfolio):
|
||||
return {
|
||||
**portfolio.to_dictionary(),
|
||||
"billing_account_name": app.config.get("AZURE_BILLING_ACCOUNT_NAME"),
|
||||
}
|
||||
|
||||
|
||||
def do_provision_portfolio(csp: CloudProviderInterface, portfolio_id=None):
|
||||
portfolio = Portfolios.get_for_update(portfolio_id)
|
||||
fsm = Portfolios.get_or_create_state_machine(portfolio)
|
||||
fsm.trigger_next_transition(csp_data=portfolio.to_dictionary())
|
||||
fsm.trigger_next_transition(csp_data=make_initial_csp_data(portfolio))
|
||||
if fsm.current_state == FSMStates.COMPLETED:
|
||||
send_PPOC_email(portfolio.to_dictionary())
|
||||
|
||||
@ -333,7 +340,7 @@ def create_billing_instruction(self):
|
||||
initial_clin_amount=clin.obligated_amount,
|
||||
initial_clin_start_date=str(clin.start_date),
|
||||
initial_clin_end_date=str(clin.end_date),
|
||||
initial_clin_type=clin.number,
|
||||
initial_clin_type=clin.jedi_clin_number,
|
||||
initial_task_order_id=str(clin.task_order_id),
|
||||
)
|
||||
|
||||
|
@ -65,6 +65,10 @@ class CLIN(Base, mixins.TimestampsMixin):
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def jedi_clin_number(self):
|
||||
return self.jedi_clin_type.value[-1]
|
||||
|
||||
def to_dictionary(self):
|
||||
data = {
|
||||
c.name: getattr(self, c.name)
|
||||
@ -77,5 +81,5 @@ class CLIN(Base, mixins.TimestampsMixin):
|
||||
@property
|
||||
def is_active(self):
|
||||
return (
|
||||
self.start_date <= pendulum.today() <= self.end_date
|
||||
self.start_date <= pendulum.today().date() <= self.end_date
|
||||
) and self.task_order.signed_at
|
||||
|
@ -1,23 +1,23 @@
|
||||
import re
|
||||
from string import ascii_lowercase, digits
|
||||
from random import choices
|
||||
from itertools import chain
|
||||
from random import choices
|
||||
from string import ascii_lowercase, digits
|
||||
from typing import Dict
|
||||
|
||||
from sqlalchemy import Column, String
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.types import ARRAY
|
||||
|
||||
from atst.models.base import Base
|
||||
import atst.models.types as types
|
||||
import atst.models.mixins as mixins
|
||||
from atst.models.task_order import TaskOrder
|
||||
from atst.models.portfolio_role import PortfolioRole, Status as PortfolioRoleStatus
|
||||
from atst.domain.permission_sets import PermissionSets
|
||||
from atst.utils import first_or_none
|
||||
from atst.database import db
|
||||
|
||||
from sqlalchemy_json import NestedMutableJson
|
||||
|
||||
from atst.database import db
|
||||
import atst.models.mixins as mixins
|
||||
import atst.models.types as types
|
||||
from atst.domain.permission_sets import PermissionSets
|
||||
from atst.models.base import Base
|
||||
from atst.models.portfolio_role import PortfolioRole
|
||||
from atst.models.portfolio_role import Status as PortfolioRoleStatus
|
||||
from atst.utils import first_or_none
|
||||
|
||||
|
||||
class Portfolio(
|
||||
Base, mixins.TimestampsMixin, mixins.AuditableMixin, mixins.DeletableMixin
|
||||
@ -54,6 +54,7 @@ class Portfolio(
|
||||
roles = relationship("PortfolioRole")
|
||||
|
||||
task_orders = relationship("TaskOrder")
|
||||
clins = relationship("CLIN", secondary="task_orders")
|
||||
|
||||
@property
|
||||
def owner_role(self):
|
||||
@ -82,13 +83,27 @@ class Portfolio(
|
||||
return len(self.task_orders)
|
||||
|
||||
@property
|
||||
def active_clins(self):
|
||||
return [
|
||||
clin
|
||||
for task_order in self.task_orders
|
||||
for clin in task_order.clins
|
||||
if clin.is_active
|
||||
]
|
||||
def initial_clin_dict(self) -> Dict:
|
||||
initial_clin = min(
|
||||
(
|
||||
clin
|
||||
for clin in self.clins
|
||||
if (clin.is_active and clin.task_order.is_signed)
|
||||
),
|
||||
key=lambda clin: clin.start_date,
|
||||
default=None,
|
||||
)
|
||||
if initial_clin:
|
||||
return {
|
||||
"initial_task_order_id": initial_clin.task_order.number,
|
||||
"initial_clin_number": initial_clin.number,
|
||||
"initial_clin_type": initial_clin.jedi_clin_number,
|
||||
"initial_clin_amount": initial_clin.obligated_amount,
|
||||
"initial_clin_start_date": initial_clin.start_date.strftime("%Y/%m/%d"),
|
||||
"initial_clin_end_date": initial_clin.end_date.strftime("%Y/%m/%d"),
|
||||
}
|
||||
else:
|
||||
return {}
|
||||
|
||||
@property
|
||||
def active_task_orders(self):
|
||||
@ -170,25 +185,33 @@ class Portfolio(
|
||||
def portfolio_id(self):
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def domain_name(self):
|
||||
"""
|
||||
CSP domain name associated with portfolio.
|
||||
If a domain name is not set, generate one.
|
||||
"""
|
||||
domain_name = re.sub("[^0-9a-zA-Z]+", "", self.name).lower() + "".join(
|
||||
choices(ascii_lowercase + digits, k=4)
|
||||
)
|
||||
if self.csp_data:
|
||||
return self.csp_data.get("domain_name", domain_name)
|
||||
else:
|
||||
return domain_name
|
||||
|
||||
@property
|
||||
def application_id(self):
|
||||
return None
|
||||
|
||||
def to_dictionary(self):
|
||||
ppoc = self.owner
|
||||
user_id = f"{ppoc.first_name[0]}{ppoc.last_name}".lower()
|
||||
|
||||
domain_name = re.sub("[^0-9a-zA-Z]+", "", self.name).lower() + "".join(
|
||||
choices(ascii_lowercase + digits, k=4)
|
||||
)
|
||||
portfolio_data = {
|
||||
"user_id": user_id,
|
||||
return {
|
||||
"user_id": f"{self.owner.first_name[0]}{self.owner.last_name}".lower(),
|
||||
"password": "",
|
||||
"domain_name": domain_name,
|
||||
"first_name": ppoc.first_name,
|
||||
"last_name": ppoc.last_name,
|
||||
"domain_name": self.domain_name,
|
||||
"first_name": self.owner.first_name,
|
||||
"last_name": self.owner.last_name,
|
||||
"country_code": "US",
|
||||
"password_recovery_email_address": ppoc.email,
|
||||
"password_recovery_email_address": self.owner.email,
|
||||
"address": { # TODO: TBD if we're sourcing this from data or config
|
||||
"company_name": "",
|
||||
"address_line_1": "",
|
||||
@ -198,27 +221,9 @@ class Portfolio(
|
||||
"postal_code": "",
|
||||
},
|
||||
"billing_profile_display_name": "ATAT Billing Profile",
|
||||
**self.initial_clin_dict,
|
||||
}
|
||||
|
||||
try:
|
||||
initial_task_order: TaskOrder = self.task_orders[0]
|
||||
initial_clin = initial_task_order.sorted_clins[0]
|
||||
portfolio_data.update(
|
||||
{
|
||||
"initial_clin_amount": initial_clin.obligated_amount,
|
||||
"initial_clin_start_date": initial_clin.start_date.strftime(
|
||||
"%Y/%m/%d"
|
||||
),
|
||||
"initial_clin_end_date": initial_clin.end_date.strftime("%Y/%m/%d"),
|
||||
"initial_clin_type": initial_clin.number,
|
||||
"initial_task_order_id": initial_task_order.number,
|
||||
}
|
||||
)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return portfolio_data
|
||||
|
||||
def __repr__(self):
|
||||
return "<Portfolio(name='{}', user_count='{}', id='{}')>".format(
|
||||
self.name, self.user_count, self.id
|
||||
|
@ -3,12 +3,13 @@ from enum import Enum
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, String
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from atst.models.clin import CLIN
|
||||
from atst.models.base import Base
|
||||
import atst.models.types as types
|
||||
import atst.models.mixins as mixins
|
||||
from atst.models.attachment import Attachment
|
||||
from pendulum import today
|
||||
from sqlalchemy import func
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
@ -41,15 +42,13 @@ class TaskOrder(Base, mixins.TimestampsMixin):
|
||||
number = Column(String, unique=True,) # Task Order Number
|
||||
signer_dod_id = Column(String)
|
||||
signed_at = Column(DateTime)
|
||||
|
||||
clins = relationship(
|
||||
"CLIN", back_populates="task_order", cascade="all, delete-orphan"
|
||||
"CLIN",
|
||||
back_populates="task_order",
|
||||
cascade="all, delete-orphan",
|
||||
order_by=lambda: [func.substr(CLIN.number, 2), func.substr(CLIN.number, 1, 2)],
|
||||
)
|
||||
|
||||
@property
|
||||
def sorted_clins(self):
|
||||
return sorted(self.clins, key=lambda clin: (clin.number[1:], clin.number[0]))
|
||||
|
||||
@hybrid_property
|
||||
def pdf(self):
|
||||
return self._pdf
|
||||
|
@ -2,6 +2,7 @@
|
||||
ASSETS_URL
|
||||
AZURE_AADP_QTY=5
|
||||
AZURE_ACCOUNT_NAME
|
||||
AZURE_BILLING_ACCOUNT_NAME
|
||||
AZURE_CLIENT_ID
|
||||
AZURE_CALC_CLIENT_ID
|
||||
AZURE_CALC_RESOURCE="http://azurecom.onmicrosoft.com/acom-prod/"
|
||||
|
@ -57,7 +57,7 @@
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for clin in task_order.sorted_clins %}
|
||||
{% for clin in task_order.clins %}
|
||||
<tr>
|
||||
<td>{{ clin.number }}</td>
|
||||
<td>{{ clin.type }}</td>
|
||||
|
@ -15,7 +15,6 @@ from tests.factories import (
|
||||
PortfolioStateMachineFactory,
|
||||
TaskOrderFactory,
|
||||
UserFactory,
|
||||
get_portfolio_csp_data,
|
||||
)
|
||||
|
||||
from atst.models import FSMStates, PortfolioStateMachine, TaskOrder
|
||||
@ -82,7 +81,7 @@ def test_state_machine_trigger_next_transition(state_machine):
|
||||
|
||||
state_machine.state = FSMStates.STARTED
|
||||
state_machine.trigger_next_transition(
|
||||
csp_data=get_portfolio_csp_data(state_machine.portfolio)
|
||||
csp_data=state_machine.portfolio.to_dictionary()
|
||||
)
|
||||
assert state_machine.current_state == FSMStates.TENANT_CREATED
|
||||
|
||||
@ -319,7 +318,11 @@ def test_fsm_transition_start(
|
||||
config = {"billing_account_name": "billing_account_name"}
|
||||
|
||||
assert state_machine.state == FSMStates.UNSTARTED
|
||||
portfolio_data = get_portfolio_csp_data(portfolio)
|
||||
portfolio_data = {
|
||||
**portfolio.to_dictionary(),
|
||||
"display_name": "mgmt group display name",
|
||||
"management_group_name": "mgmt-group-uuid",
|
||||
}
|
||||
|
||||
for expected_state in expected_states:
|
||||
collected_data = dict(
|
||||
|
@ -79,49 +79,6 @@ def get_all_portfolio_permission_sets():
|
||||
return PermissionSets.get_many(PortfolioRoles.PORTFOLIO_PERMISSION_SETS)
|
||||
|
||||
|
||||
def get_portfolio_csp_data(portfolio):
|
||||
|
||||
ppoc = portfolio.owner
|
||||
if not ppoc:
|
||||
|
||||
class ppoc:
|
||||
first_name = "John"
|
||||
last_name = "Doe"
|
||||
email = "email@example.com"
|
||||
|
||||
user_id = f"{ppoc.first_name[0]}{ppoc.last_name}".lower()
|
||||
domain_name = re.sub("[^0-9a-zA-Z]+", "", portfolio.name).lower()
|
||||
|
||||
initial_task_order: TaskOrder = portfolio.task_orders[0]
|
||||
initial_clin = initial_task_order.sorted_clins[0]
|
||||
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"password": "jklfsdNCVD83nklds2#202", # pragma: allowlist secret
|
||||
"domain_name": domain_name,
|
||||
"display_name": "mgmt group display name",
|
||||
"management_group_name": "mgmt-group-uuid",
|
||||
"first_name": ppoc.first_name,
|
||||
"last_name": ppoc.last_name,
|
||||
"country_code": "US",
|
||||
"password_recovery_email_address": ppoc.email,
|
||||
"address": { # TODO: TBD if we're sourcing this from data or config
|
||||
"company_name": "",
|
||||
"address_line_1": "",
|
||||
"city": "",
|
||||
"region": "",
|
||||
"country": "",
|
||||
"postal_code": "",
|
||||
},
|
||||
"billing_profile_display_name": "My Billing Profile",
|
||||
"initial_clin_amount": initial_clin.obligated_amount,
|
||||
"initial_clin_start_date": initial_clin.start_date.strftime("%Y/%m/%d"),
|
||||
"initial_clin_end_date": initial_clin.end_date.strftime("%Y/%m/%d"),
|
||||
"initial_clin_type": initial_clin.number,
|
||||
"initial_task_order_id": initial_task_order.number,
|
||||
}
|
||||
|
||||
|
||||
class Base(factory.alchemy.SQLAlchemyModelFactory):
|
||||
@classmethod
|
||||
def dictionary(cls, **attrs):
|
||||
|
@ -179,3 +179,58 @@ class TestUpcomingObligatedFunds:
|
||||
)
|
||||
# Only sums the upcoming task order
|
||||
assert portfolio.upcoming_obligated_funds == Decimal(700.0)
|
||||
|
||||
|
||||
class TestInitialClinDict:
|
||||
def test_formats_dict_correctly(self):
|
||||
portfolio = PortfolioFactory()
|
||||
task_order = TaskOrderFactory(
|
||||
portfolio=portfolio, number="1234567890123", signed_at=pendulum.now()
|
||||
)
|
||||
clin = CLINFactory(task_order=task_order)
|
||||
initial_clin = portfolio.initial_clin_dict
|
||||
|
||||
assert initial_clin["initial_clin_amount"] == clin.obligated_amount
|
||||
assert initial_clin["initial_clin_start_date"] == clin.start_date.strftime(
|
||||
"%Y/%m/%d"
|
||||
)
|
||||
assert initial_clin["initial_clin_end_date"] == clin.end_date.strftime(
|
||||
"%Y/%m/%d"
|
||||
)
|
||||
assert initial_clin["initial_clin_type"] == clin.jedi_clin_number
|
||||
assert initial_clin["initial_clin_number"] == clin.number
|
||||
assert initial_clin["initial_task_order_id"] == task_order.number
|
||||
|
||||
def test_no_valid_clins(self):
|
||||
portfolio = PortfolioFactory()
|
||||
assert portfolio.initial_clin_dict == {}
|
||||
|
||||
def test_picks_the_initial_clin(self):
|
||||
yesterday = pendulum.now().subtract(days=1).date()
|
||||
tomorrow = pendulum.now().add(days=1).date()
|
||||
portfolio = PortfolioFactory(
|
||||
task_orders=[
|
||||
{
|
||||
"signed_at": pendulum.now(),
|
||||
"create_clins": [
|
||||
{
|
||||
"number": "0001",
|
||||
"start_date": yesterday.subtract(days=1),
|
||||
"end_date": yesterday,
|
||||
},
|
||||
{
|
||||
"number": "1001",
|
||||
"start_date": yesterday,
|
||||
"end_date": tomorrow,
|
||||
},
|
||||
{
|
||||
"number": "0002",
|
||||
"start_date": yesterday,
|
||||
"end_date": tomorrow,
|
||||
},
|
||||
],
|
||||
},
|
||||
{"create_clins": [{"number": "0003"}]},
|
||||
],
|
||||
)
|
||||
assert portfolio.initial_clin_dict["initial_clin_number"] == "1001"
|
||||
|
@ -62,7 +62,7 @@ def test_clin_sorting():
|
||||
CLINFactory.create(number="2001"),
|
||||
]
|
||||
)
|
||||
assert [clin.number for clin in task_order.sorted_clins] == [
|
||||
assert [clin.number for clin in task_order.clins] == [
|
||||
"0001",
|
||||
"1001",
|
||||
"2001",
|
||||
|
Loading…
x
Reference in New Issue
Block a user