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:
dandds 2020-02-26 11:20:19 -05:00 committed by GitHub
commit f8dd072340
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 140 additions and 108 deletions

View File

@ -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

View File

@ -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),
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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/"

View File

@ -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>

View File

@ -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(

View File

@ -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):

View File

@ -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"

View File

@ -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",