def send_decline_master_agreement_email(supplier_code):
    from app.api.services import (audit_service, audit_types, suppliers)

    supplier = suppliers.get_supplier_by_code(supplier_code)
    to_addresses = [
        e['email_address']
        for e in suppliers.get_supplier_contacts(supplier_code)
    ]

    # prepare copy
    email_body = render_email_template(
        'seller_edit_decline.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'])

    subject = "You declined the new Master Agreement"

    send_or_handle_error(
        to_addresses,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors='declined master agreement email')

    audit_service.log_audit_event(
        audit_type=audit_types.declined_master_agreement_email,
        user='',
        data={
            "to_address": to_addresses,
            "email_body": email_body,
            "subject": subject
        },
        db_object=supplier)
def create(current_user):
    lot = lots_service.find(slug='training2').one_or_none()
    framework = frameworks_service.find(
        slug='digital-marketplace').one_or_none()
    user = users.get(current_user.id)
    agency_name = ''

    email_domain = user.email_address.split('@')[1]
    agency = agency_service.find(domain=email_domain).one_or_none()
    if agency:
        agency_name = agency.name

    domain = domain_service.find(
        name='Training, Learning and Development').one_or_none()
    seller_category = None
    if domain:
        seller_category = str(domain.id)
    else:
        raise Exception('Training, Learning and Development domain not found')

    brief = briefs.create_brief(user,
                                current_user.get_team(),
                                framework,
                                lot,
                                data={
                                    'organisation': agency_name,
                                    'sellerCategory': seller_category
                                })

    audit_service.log_audit_event(audit_type=audit_types.create_brief,
                                  user=current_user.email_address,
                                  data={'briefId': brief.id},
                                  db_object=brief)

    return brief
def send_removed_team_member_notification_emails(team_id, user_ids):
    team = team_service.find(id=team_id).first()

    to_addresses = []
    for user_id in user_ids:
        user = users.get(user_id)
        to_addresses.append(user.email_address)

    if len(to_addresses) == 0:
        return

    email_body = render_email_template(
        'team_member_removed.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        team_lead=escape_markdown(current_user.name),
        team_name=escape_markdown(team.name))

    subject = 'You have been removed from the {} team'.format(
        team.name.encode('utf-8'))

    send_or_handle_error(
        to_addresses,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors='team member removed email')

    audit_service.log_audit_event(audit_type=audit_types.team_member_removed,
                                  data={
                                      'to_address': to_addresses,
                                      'subject': subject
                                  },
                                  db_object=team,
                                  user='')
def send_seller_requested_feedback_from_buyer_email(brief):
    from app.api.services import audit_service, audit_types  # to circumvent circular dependency

    to_addresses = get_brief_emails(brief)

    # prepare copy
    email_body = render_email_template(
        'seller_requested_feedback_from_buyer_email.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        brief_name=brief.data['title'],
        brief_id=brief.id
    )

    subject = "Buyer notifications to unsuccessful sellers"

    send_or_handle_error(
        to_addresses,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors='seller_requested_feedback_from_buyer_email'
    )

    audit_service.log_audit_event(
        audit_type=audit_types.seller_requested_feedback_from_buyer_email,
        user='',
        data={
            "to_addresses": ', '.join(to_addresses),
            "email_body": email_body,
            "subject": subject
        },
        db_object=brief)
Example #5
0
def publish_answer(current_user, brief_id, data):
    brief = briefs.get(brief_id)
    if not brief:
        raise NotFoundError("Invalid brief id '{}'".format(brief_id))

    if not briefs.has_permission_to_brief(current_user.id, brief.id):
        raise UnauthorisedError('Unauthorised to publish answer')

    publish_question = data.get('question')
    if not publish_question:
        raise ValidationError('Question is required')

    answer = data.get('answer')
    if not answer:
        raise ValidationError('Answer is required')

    brief_clarification_question = brief_clarification_question_service.save(
        BriefClarificationQuestion(_brief_id=brief.id,
                                   question=publish_question,
                                   answer=answer,
                                   user_id=current_user.id))

    question_id = data.get('questionId')
    if question_id:
        question = brief_question_service.get(question_id)
        if question.brief_id == brief.id:
            question.answered = True
            brief_question_service.save(question)

    audit_service.log_audit_event(
        audit_type=audit_types.create_brief_clarification_question,
        user=current_user.email_address,
        data={'briefId': brief.id},
        db_object=brief_clarification_question)
def close_opportunity_early(user_id, brief_id):
    brief = brief_service.get(brief_id)
    if not brief:
        raise NotFoundError('Opportunity {} does not exist'.format(brief_id))

    if not brief_service.has_permission_to_brief(user_id, brief_id):
        raise UnauthorisedError(
            'Not authorised to close opportunity {}'.format(brief_id))

    if not can_close_opportunity_early(brief):
        raise BriefError('Unable to close opportunity {}'.format(brief_id))

    user = users.get(user_id)
    if not user:
        raise NotFoundError('User {} does not exist'.format(user_id))

    brief = brief_service.close_opportunity_early(brief)
    create_responses_zip(brief.id)
    send_opportunity_closed_early_email(brief, user)

    try:
        audit_service.log_audit_event(
            audit_type=audit_types.close_opportunity_early,
            data={'briefId': brief.id},
            db_object=brief,
            user=user.email_address)

        publish_tasks.brief.delay(publish_tasks.compress_brief(brief),
                                  'closed_early',
                                  email_address=user.email_address,
                                  name=user.name)
    except Exception as e:
        rollbar.report_exc_info()

    return brief
def send_seller_requested_feedback_from_buyer_email(brief):
    from app.api.services import audit_service, audit_types  # to circumvent circular dependency

    to_addresses = [user.email_address for user in brief.users if user.active]

    # prepare copy
    email_body = render_email_template(
        'seller_requested_feedback_from_buyer_email.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        brief_name=brief.data['title'],
        brief_id=brief.id
    )

    subject = "Buyer notifications to unsuccessful sellers"

    send_or_handle_error(
        to_addresses,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors='seller_requested_feedback_from_buyer_email'
    )

    audit_service.log_audit_event(
        audit_type=audit_types.seller_requested_feedback_from_buyer_email,
        user='',
        data={
            "to_addresses": ', '.join(to_addresses),
            "email_body": email_body,
            "subject": subject
        },
        db_object=brief)
def create_team():
    user = users.get(current_user.id)
    created_teams = team_service.get_teams_for_user(user.id, 'created')
    completed_teams = team_service.get_teams_for_user(user.id)

    if len(completed_teams) == 0:
        if len(created_teams) == 0:
            team = team_service.save(
                Team(name='',
                     status='created',
                     team_members=[
                         TeamMember(user_id=user.id, is_team_lead=True)
                     ]))

            audit_service.log_audit_event(audit_type=audit_types.create_team,
                                          data={},
                                          db_object=team,
                                          user=current_user.email_address)

            publish_tasks.team.delay(publish_tasks.compress_team(team),
                                     'created')

            return get_team(team.id)

        created_team = created_teams.pop()
        return get_team(created_team.id)
    else:
        team = completed_teams[0]
        raise TeamError(
            'You can only be in one team. You\'re already a member of {}.'.
            format(team.name))
def decline_agreement(user_info):
    supplier_code = user_info.get('supplier_code')
    email_address = user_info.get('email_address')

    supplier = suppliers.get_supplier_by_code(supplier_code)
    mandatory_supplier_checks(supplier)

    if email_address != supplier.data.get('email'):
        raise UnauthorisedError('Unauthorised to decline agreement')

    supplier_users = users.find(supplier_code=supplier_code).all()
    supplier.status = 'deleted'
    for user in supplier_users:
        user.active = False
        users.add_to_session(user)

    users.commit_changes()
    suppliers.save(supplier)

    send_decline_master_agreement_email(supplier.code)

    publish_tasks.supplier.delay(publish_tasks.compress_supplier(supplier),
                                 'declined_agreement',
                                 updated_by=email_address)

    audit_service.log_audit_event(
        audit_type=audit_types.declined_master_agreement,
        user=email_address,
        data={
            'supplierCode': supplier.code,
            'supplierData': supplier.data
        },
        db_object=supplier)
def send_brief_clarification_to_seller(brief, brief_question, to_address):
    from app.api.services import (audit_service, audit_types
                                  )  # to circumvent circular dependency

    # prepare copy
    email_body = render_email_template(
        'brief_question_to_seller.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        brief_id=brief.id,
        brief_name=escape_markdown(brief.data.get('title')),
        brief_organisation=brief.data.get('organisation'),
        publish_by_date=brief.questions_closed_at.strftime('%d/%m/%Y'),
        message=escape_markdown(brief_question.data.get('question')))

    subject = u"You submitted a question for {} ({}) successfully".format(
        brief.data.get('title'), brief.id)

    send_or_handle_error(
        to_address,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors='brief question email sent to seller')

    audit_service.log_audit_event(
        audit_type=audit_types.sent_brief_question_to_seller,
        user='',
        data={
            "to_addresses": to_address,
            "email_body": email_body,
            "subject": subject
        },
        db_object=brief)
def post_brief_response(brief_id):

    brief_response_json = get_json_from_request()
    supplier, brief = _can_do_brief_response(brief_id)
    try:
        brief_response = BriefResponse(
            data=brief_response_json,
            supplier=supplier,
            brief=brief
        )

        brief_response.validate()
        db.session.add(brief_response)
        db.session.flush()

    except ValidationError as e:
        brief_response_json['brief_id'] = brief_id
        rollbar.report_exc_info(extra_data=brief_response_json)
        message = ""
        if 'essentialRequirements' in e.message and e.message['essentialRequirements'] == 'answer_required':
            message = "Essential requirements must be completed"
            del e.message['essentialRequirements']
        if 'attachedDocumentURL' in e.message:
            if e.message['attachedDocumentURL'] == 'answer_required':
                message = "Documents must be uploaded"
            if e.message['attachedDocumentURL'] == 'file_incorrect_format':
                message = "Uploaded documents are in the wrong format"
            del e.message['attachedDocumentURL']
        if 'criteria' in e.message and e.message['criteria'] == 'answer_required':
            message = "Criteria must be completed"
        if len(e.message) > 0:
            message += json.dumps(e.message)
        return jsonify(message=message), 400
    except Exception as e:
        brief_response_json['brief_id'] = brief_id
        rollbar.report_exc_info(extra_data=brief_response_json)
        return jsonify(message=e.message), 400

    try:
        send_brief_response_received_email(supplier, brief, brief_response)
    except Exception as e:
        brief_response_json['brief_id'] = brief_id
        rollbar.report_exc_info(extra_data=brief_response_json)

    audit_service.log_audit_event(
        audit_type=AuditTypes.create_brief_response,
        user=current_user.email_address,
        data={
            'briefResponseId': brief_response.id,
            'briefResponseJson': brief_response_json,
        },
        db_object=brief_response)

    publish_tasks.brief_response.delay(
        publish_tasks.compress_brief_response(brief_response),
        'submitted',
        user=current_user.email_address
    )
    return jsonify(briefResponses=brief_response.serialize()), 201
def send_opportunity_withdrawn_email_to_buyers(brief, current_user):
    # to circumvent circular dependencies
    from app.api.business.brief import brief_business
    from app.api.services import audit_service, audit_types

    to_addresses = get_brief_emails(brief)
    seller_message = ''
    invited_seller_codes = brief.data.get('sellers', {}).keys()

    if brief_business.is_open_to_all(brief):
        seller_message = 'We have notified sellers who have drafted or submitted responses to this opportunity'
    elif len(invited_seller_codes) == 1:
        invited_seller_code = invited_seller_codes.pop()
        seller_name = brief.data['sellers'][invited_seller_code]['name']
        seller_message = '{} has been notified'.format(seller_name)
    else:
        seller_message = 'All invited sellers have been notified'

    email_body = render_email_template(
        'opportunity_withdrawn_buyers.md',
        brief_id=brief.id,
        framework=brief.framework.slug,
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        seller_message=escape_markdown(seller_message),
        title=escape_markdown(brief.data['title']),
        user=escape_markdown(current_user.name),
        withdrawal_reason=escape_markdown(brief.data['reasonToWithdraw'])
    )

    subject = "'{}' ({}) is withdrawn from the Digital Marketplace".format(
        brief.data['title'],
        brief.id
    )

    send_or_handle_error(
        to_addresses,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors=audit_types.withdraw_opportunity
    )

    audit_service.log_audit_event(
        audit_type=audit_types.sent_opportunity_withdrawn_email_to_buyers,
        user='',
        data={
            "to_addresses": ', '.join(to_addresses),
            "email_body": email_body,
            "subject": subject
        },
        db_object=brief
    )
Example #13
0
def send_brief_closed_email(brief):
    from app.api.services import audit_service, audit_types  # to circumvent circular dependency
    from app.tasks.s3 import create_resumes_zip, CreateResumesZipException

    brief_email_sent_audit_event = audit_service.find(
        type=audit_types.sent_closed_brief_email.value,
        object_type="Brief",
        object_id=brief.id).count()

    if (brief_email_sent_audit_event > 0):
        return

    # create the resumes zip
    has_resumes_zip = False
    try:
        create_resumes_zip(brief.id)
        has_resumes_zip = True
    except Exception as e:
        rollbar.report_exc_info()
        pass

    to_addresses = [
        user.email_address for user in brief.users
        if user.active and user.email_address.endswith('@digital.gov.au')
    ]

    # prepare copy
    email_body = render_email_template(
        'brief_closed.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        brief_name=brief.data['title'],
        brief_id=brief.id,
        has_resumes_zip=has_resumes_zip)

    subject = "Your brief has closed - please review all responses."

    send_or_handle_error(to_addresses,
                         email_body,
                         subject,
                         current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
                         current_app.config['DM_GENERIC_SUPPORT_NAME'],
                         event_description_for_errors='brief closed')

    audit_service.log_audit_event(
        audit_type=audit_types.sent_closed_brief_email,
        user='',
        data={
            "to_addresses": ', '.join(to_addresses),
            "email_body": email_body,
            "subject": subject
        },
        db_object=brief)
def send_specialist_brief_response_withdrawn_email(supplier, brief, brief_response, supplier_user=None):
    from app.api.services import audit_service, audit_types  # to circumvent circular dependency

    to_address = brief_response.data['respondToEmailAddress']

    specialist_name = '{} {}'.format(
        brief_response.data.get('specialistGivenNames', ''),
        brief_response.data.get('specialistSurname', '')
    )

    subject = "{}'s response to '{}' ({}) has been withdrawn".format(
        specialist_name,
        brief.data['title'],
        brief.id
    )

    brief_url = '{}/2/{}/opportunities/{}'.format(
        current_app.config['FRONTEND_ADDRESS'],
        brief.framework.slug,
        brief.id
    )

    email_body = render_email_template(
        'specialist_brief_response_withdrawn.md',
        specialist_name=specialist_name,
        brief_url=brief_url,
        brief_name=brief.data['title'],
        brief_id=brief.id,
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        brief_organisation=brief.data['organisation'],
        supplier_user=supplier_user
    )

    send_or_handle_error(
        to_address,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors='brief response withdrawn'
    )

    audit_service.log_audit_event(
        audit_type=audit_types.specialist_brief_response_withdrawn_email,
        user='',
        data={
            "to_address": to_address,
            "email_body": email_body,
            "subject": subject
        },
        db_object=brief_response)
def update(agency_id, agency, updated_by):
    existing = agency_service.get_agency_for_update(agency_id)

    if agency.get('name'):
        existing.name = agency.get('name')

    if agency.get('category'):
        existing.category = agency.get('category')

    if agency.get('bodyType'):
        existing.body_type = agency.get('bodyType')

    if agency.get('whitelisted', None) is not None:
        existing.whitelisted = agency.get('whitelisted')

    if agency.get('reports', None) is not None:
        existing.reports = agency.get('reports')

    if agency.get('must_join_team', None) is not None:
        existing.must_join_team = agency.get('must_join_team')

    if agency.get('state'):
        existing.state = agency.get('state')

    if agency.get('domains', None) is not None:
        domains = agency.get('domains', [])
        to_remove = []
        to_add = []
        for e in existing.domains:
            if e.domain not in domains:
                to_remove.append(e)

        for d in domains:
            if d not in [e.domain for e in existing.domains]:
                to_add.append(AgencyDomain(active=True, domain=d))

        for e in to_remove:
            existing.domains.remove(e)
        for e in to_add:
            existing.domains.append(e)

    updated = agency_service.save(existing)
    result = get_agency(updated.id)
    audit_service.log_audit_event(audit_type=audit_types.agency_updated,
                                  user=updated_by,
                                  data={
                                      'incoming': agency,
                                      'saved': result
                                  },
                                  db_object=updated)
    return result
def send_specialist_brief_closed_email(brief):
    from app.api.services import (
        audit_service,
        audit_types,
        brief_responses_service
    )  # to circumvent circular dependency

    if brief.lot.slug != 'specialist':
        return

    audit_event = audit_service.find(type=audit_types.specialist_brief_closed_email.value,
                                     object_type="Brief",
                                     object_id=brief.id).count()

    if (audit_event > 0):
        return

    responses = brief_responses_service.get_brief_responses(brief.id, None, submitted_only=True)
    to_addresses = get_brief_emails(brief)

    # prepare copy
    email_body = render_email_template(
        'specialist_brief_closed.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        brief_name=brief.data['title'],
        brief_id=brief.id,
        number_of_responses='{}'.format(len(responses)),
        number_of_responses_plural='s' if len(responses) > 1 else ''
    )

    subject = 'Your "{}" opportunity has closed.'.format(brief.data['title'])

    send_or_handle_error(
        to_addresses,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors='brief closed'
    )

    audit_service.log_audit_event(
        audit_type=audit_types.specialist_brief_closed_email,
        user='',
        data={
            "to_addresses": ', '.join(to_addresses),
            "email_body": email_body,
            "subject": subject
        },
        db_object=brief)
def create_atm_brief():
    """Create ATM brief (role=buyer)
    ---
    tags:
        - brief
    definitions:
        ATMBriefCreated:
            type: object
            properties:
                id:
                    type: number
                lot:
                    type: string
                status:
                    type: string
                author:
                    type: string
    responses:
        200:
            description: Brief created successfully.
            schema:
                $ref: '#/definitions/ATMBriefCreated'
        400:
            description: Bad request.
        403:
            description: Unauthorised to create ATM brief.
        500:
            description: Unexpected error.
    """
    try:
        lot = lots_service.find(slug='atm').one_or_none()
        framework = frameworks_service.find(slug='digital-marketplace').one_or_none()
        user = users.get(current_user.id)
        brief = briefs.create_brief(user, framework, lot)
    except Exception as e:
        rollbar.report_exc_info()
        return jsonify(message=e.message), 400

    try:
        audit_service.log_audit_event(
            audit_type=AuditTypes.create_brief,
            user=current_user.email_address,
            data={
                'briefId': brief.id
            },
            db_object=brief)
    except Exception as e:
        rollbar.report_exc_info()

    return jsonify(brief.serialize(with_users=False))
Example #18
0
def send_notify_auth_rep_email(supplier_code):
    from app.api.services import (
        audit_service,
        audit_types,
        suppliers,
        key_values_service
    )

    supplier = suppliers.get_supplier_by_code(supplier_code)
    to_address = supplier.data.get('email', '').encode('utf-8')

    agreement = get_new_agreement()
    if agreement is None:
        agreement = get_current_agreement()

    start_date = pendulum.now('Australia/Canberra').date()
    if agreement:
        start_date = agreement.start_date.in_tz('Australia/Canberra')

    # prepare copy
    email_body = render_email_template(
        'seller_edit_notify_auth_rep.md',
        ma_start_date=start_date.strftime('%-d %B %Y'),
        supplier_name=supplier.name,
        supplier_code=supplier.code,
        auth_rep_name=escape_markdown(supplier.data.get('representative', '')),
        frontend_url=current_app.config['FRONTEND_ADDRESS']
    )

    subject = "Accept the Master Agreement for the Digital Marketplace"

    send_or_handle_error(
        to_address,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors='notify auth rep email'
    )

    audit_service.log_audit_event(
        audit_type=audit_types.notify_auth_rep_accept_master_agreement,
        user='',
        data={
            "to_address": to_address,
            "email_body": email_body,
            "subject": subject
        },
        db_object=supplier)
def withdraw_opportunity(user_id, brief_id, withdrawal_reason):
    brief = brief_service.get(brief_id)
    if not brief:
        raise NotFoundError('Opportunity {} does not exist'.format(brief_id))

    if not brief_service.has_permission_to_brief(user_id, brief_id):
        raise UnauthorisedError(
            'Not authorised to withdraw opportunity {}'.format(brief_id))

    if brief.status != 'live':
        raise BriefError('Unable to withdraw opportunity {}'.format(brief_id))

    if not withdrawal_reason:
        raise ValidationError(
            'Withdrawal reason is required for opportunity {}'.format(
                brief_id))

    user = users.get(user_id)
    if not user:
        raise NotFoundError('User {} does not exist'.format(user_id))

    brief = brief_service.withdraw_opportunity(brief, withdrawal_reason)
    organisation = agency_service.get_agency_name(user.agency_id)
    sellers_to_contact = brief_service.get_sellers_to_notify(
        brief, brief_business.is_open_to_all(brief))

    for email_address in sellers_to_contact:
        send_opportunity_withdrawn_email_to_seller(brief, email_address,
                                                   organisation)

    send_opportunity_withdrawn_email_to_buyers(brief, user)

    try:
        audit_service.log_audit_event(
            audit_type=audit_types.withdraw_opportunity,
            data={'briefId': brief.id},
            db_object=brief,
            user=user.email_address)

        publish_tasks.brief.delay(publish_tasks.compress_brief(brief),
                                  'withdrawn',
                                  email_address=user.email_address,
                                  name=user.name)
    except Exception as e:
        rollbar.report_exc_info()

    return brief
def notify_auth_rep(user_info):
    supplier_code = user_info.get('supplier_code')
    email_address = user_info.get('email_address')

    supplier = suppliers.get_supplier_by_code(supplier_code)
    mandatory_supplier_checks(supplier)

    send_notify_auth_rep_email(supplier.code)

    audit_service.log_audit_event(
        audit_type=audit_types.notify_auth_rep_accept_master_agreement,
        user=email_address,
        data={
            'supplierCode': supplier.code,
            'supplierData': supplier.data
        },
        db_object=supplier)
def send_specialist_brief_seller_invited_email(brief, invited_supplier):
    from app.api.services import audit_service, audit_types  # to circumvent circular dependency

    if brief.lot.slug != 'specialist':
        return

    to_addresses = []
    if 'contact_email' in invited_supplier.data:
        to_addresses = [invited_supplier.data['contact_email']]
    elif 'email' in invited_supplier.data:
        to_addresses = [invited_supplier.data['email']]

    if len(to_addresses) > 0:
        number_of_suppliers = int(brief.data['numberOfSuppliers'])
        email_body = render_email_template(
            'specialist_brief_invite_seller.md',
            frontend_url=current_app.config['FRONTEND_ADDRESS'],
            brief_name=brief.data['title'],
            brief_id=brief.id,
            brief_organisation=brief.data['organisation'],
            brief_close_date=brief.closed_at.strftime('%d/%m/%Y'),
            question_close_date=brief.questions_closed_at.strftime('%d/%m/%Y'),
            number_of_suppliers=number_of_suppliers,
            number_of_suppliers_plural='s' if number_of_suppliers > 1 else ''
        )

        subject = "You're invited to submit candidates for {}".format(brief.data['title'])

        send_or_handle_error(
            to_addresses,
            email_body,
            subject,
            current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
            current_app.config['DM_GENERIC_SUPPORT_NAME'],
            event_description_for_errors='seller_invited_to_specialist_opportunity'
        )

        audit_service.log_audit_event(
            audit_type=audit_types.seller_invited_to_specialist_opportunity,
            user='',
            data={
                "to_addresses": ', '.join(to_addresses),
                "email_body": email_body,
                "subject": subject
            },
            db_object=brief)
def send_team_member_notification_emails(team_id, user_ids=None):
    team = team_service.find(id=team_id).first()

    if user_ids is None or len(user_ids) == 0:
        # Team members added through the create flow
        members = team_member_service.find(team_id=team_id,
                                           is_team_lead=False).all()
    else:
        # Team members added through the edit flow
        members = team_member_service.get_team_members_by_user_id(
            team_id, user_ids)

    to_addresses = []
    for member in members:
        user = users.get(member.user_id)
        to_addresses.append(user.email_address)

    if len(to_addresses) == 0:
        return

    email_body = render_email_template(
        'team_member_added.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        team_lead=escape_markdown(current_user.name),
        team_name=escape_markdown(team.name))

    subject = '{} added you as a member of {}'.format(
        current_user.name, team.name.encode('utf-8'))

    send_or_handle_error(
        to_addresses,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors='team member added email')

    audit_service.log_audit_event(audit_type=audit_types.team_member_added,
                                  data={
                                      'to_address': to_addresses,
                                      'subject': subject
                                  },
                                  db_object=team,
                                  user='')
def send_opportunity_edited_email_to_seller(brief, email_address, buyer):
    # to circumvent circular dependencies
    from app.api.services import audit_service, audit_types

    candidate_message = ''
    if brief.lot.slug == 'specialist':
        candidate_message = "candidate's "

    formatted_closing_date = (
        brief.closed_at.in_timezone('Australia/Canberra').format('%A %-d %B %Y at %-I:%M%p (in Canberra)')
    )

    email_body = render_email_template(
        'opportunity_edited_sellers.md',
        brief_id=brief.id,
        buyer=buyer,
        candidate_message=candidate_message,
        closing_date=formatted_closing_date,
        framework=brief.framework.slug,
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        title=escape_markdown(brief.data['title'])
    )

    subject = "Changes made to '{}' opportunity".format(brief.data['title'])

    send_or_handle_error(
        email_address,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors=audit_types.opportunity_edited
    )

    audit_service.log_audit_event(
        audit_type=audit_types.sent_opportunity_edited_email_to_seller,
        user='',
        data={
            "to_addresses": email_address,
            "email_body": email_body,
            "subject": subject
        },
        db_object=brief
    )
def send_brief_closed_email(brief):
    from app.api.services import audit_service, audit_types  # to circumvent circular dependency

    if brief.lot.slug in ['specialist']:
        return

    brief_email_sent_audit_event = audit_service.find(type=audit_types.sent_closed_brief_email.value,
                                                      object_type="Brief",
                                                      object_id=brief.id).count()

    if (brief_email_sent_audit_event > 0):
        return

    to_addresses = get_brief_emails(brief)

    # prepare copy
    email_body = render_email_template(
        'brief_closed.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        brief_name=brief.data['title'],
        brief_id=brief.id
    )

    subject = "Your opportunity has closed - please review all responses."

    send_or_handle_error(
        to_addresses,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors='brief closed'
    )

    audit_service.log_audit_event(
        audit_type=audit_types.sent_closed_brief_email,
        user='',
        data={
            "to_addresses": ', '.join(to_addresses),
            "email_body": email_body,
            "subject": subject
        },
        db_object=brief)
def send_request_access_email(permission):
    if not current_user.is_part_of_team():
        return

    user_team = current_user.get_team()

    team = team_service.find(id=user_team.get('id'),
                             status='completed').one_or_none()

    if not team:
        return

    to_addresses = [
        tm.user.email_address for tm in team.team_members if tm.is_team_lead
    ]

    if len(to_addresses) == 0:
        return

    email_body = render_email_template(
        'request_access.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        name=escape_markdown(current_user.name),
        permission=escape_markdown(permission))

    subject = '{} has requested a change to their permissions'.format(
        current_user.name)

    send_or_handle_error(to_addresses,
                         email_body,
                         subject,
                         current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
                         current_app.config['DM_GENERIC_SUPPORT_NAME'],
                         event_description_for_errors='request access email')

    audit_service.log_audit_event(audit_type=audit_types.sent_request_access,
                                  data={
                                      'to_address': to_addresses,
                                      'subject': subject,
                                      'email_body': email_body
                                  },
                                  db_object=team,
                                  user=current_user.email_address)
Example #26
0
def delete_draft_evidence(evidence_id, actioned_by):
    evidence = evidence_service.get_evidence_by_id(evidence_id)
    if not evidence or not evidence.status == 'draft':
        return False
    evidence_service.delete(evidence)
    audit_service.log_audit_event(
        audit_type=audit_types.evidence_draft_deleted,
        user=actioned_by,
        data={
            "id": evidence.id,
            "domainId": evidence.domain_id,
            "briefId": evidence.brief_id,
            "status": evidence.status,
            "supplierCode": evidence.supplier_code,
            "data": evidence.data
        },
        db_object=evidence
    )
    return True
def send_team_lead_notification_emails(team_id, user_ids=None):
    team = team_service.find(id=team_id).first()

    if user_ids is None or len(user_ids) == 0:
        # Team leads added through the create flow
        team_leads = team_member_service.find(team_id=team_id,
                                              is_team_lead=True).all()
        team_leads = [
            team_lead for team_lead in team_leads
            if team_lead.user_id != current_user.id
        ]
    else:
        # Team leads added through the edit flow
        team_leads = team_member_service.get_team_leads_by_user_id(
            team_id, user_ids)

    to_addresses = []
    for team_lead in team_leads:
        user = users.get(team_lead.user_id)
        to_addresses.append(user.email_address)

    if len(to_addresses) == 0:
        return

    email_body = render_email_template(
        'team_lead_added.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        team_name=escape_markdown(team.name))

    subject = 'You have been upgraded to a team lead'

    send_or_handle_error(to_addresses,
                         email_body,
                         subject,
                         current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
                         current_app.config['DM_GENERIC_SUPPORT_NAME'],
                         event_description_for_errors='team lead added email')

    audit_service.log_audit_event(audit_type=audit_types.team_lead_added,
                                  data={'to_address': to_addresses},
                                  db_object=team,
                                  user='')
def send_decline_request_to_join_team_leaders_email(team_id, requester_name,
                                                    requester_email, reason):
    team = team_service.find(id=team_id, status='completed').one_or_none()

    if not team:
        return

    to_addresses = [
        tm.user.email_address for tm in team.team_members if tm.is_team_lead
    ]

    if len(to_addresses) == 0:
        return

    email_body = render_email_template(
        'request_to_join_declined.md',
        lead_name=escape_markdown(current_user.name),
        requester_name=escape_markdown(requester_name),
        requester_email=escape_markdown(requester_email),
        reason=escape_markdown(reason),
        team=escape_markdown(team.name))

    extra = '' if requester_name.endswith('s') else 's'
    subject = "{}'{} request to join {} has been declined".format(
        requester_name, extra, team.name)

    send_or_handle_error(to_addresses,
                         email_body,
                         subject,
                         current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
                         current_app.config['DM_GENERIC_SUPPORT_NAME'],
                         event_description_for_errors='request to join email')

    audit_service.log_audit_event(
        audit_type=audit_types.sent_request_to_join_team_decline,
        data={
            'to_address': to_addresses,
            'subject': subject,
            'email_body': email_body
        },
        db_object=team,
        user=current_user.email_address)
def update_supplier(data, user_info):
    supplier_code = user_info.get('supplier_code')
    email_address = user_info.get('email_address')

    supplier = suppliers.find(code=supplier_code).one_or_none()
    mandatory_supplier_checks(supplier)

    whitelist_fields = ['representative', 'email', 'phone']
    for wf in whitelist_fields:
        if wf not in data.get('data'):
            raise ValidationError('{} is not recognised'.format(wf))

    if 'email' in data.get('data'):
        email = data['data']['email']
        data['data']['email'] = email.encode('utf-8').lower()

    supplier.update_from_json(data.get('data'))

    messages = SupplierValidator(supplier).validate_representative(
        'representative')
    if len([m for m in messages if m.get('severity', '') == 'error']) > 0:
        raise ValidationError(',\n'.join([
            m.get('message') for m in messages
            if m.get('severity', '') == 'error'
        ]))

    suppliers.save(supplier)

    publish_tasks.supplier.delay(publish_tasks.compress_supplier(supplier),
                                 'updated',
                                 updated_by=email_address)

    audit_service.log_audit_event(audit_type=audit_types.update_supplier,
                                  user=email_address,
                                  data={
                                      'supplierCode': supplier.code,
                                      'supplierData': supplier.data
                                  },
                                  db_object=supplier)

    process_auth_rep_email(supplier, data, user_info)
def send_brief_closed_email(brief):
    from app.api.services import audit_service, audit_types  # to circumvent circular dependency

    brief_email_sent_audit_event = audit_service.find(type=audit_types.sent_closed_brief_email.value,
                                                      object_type="Brief",
                                                      object_id=brief.id).count()

    if (brief_email_sent_audit_event > 0):
        return

    to_addresses = [user.email_address for user in brief.users if user.active]

    # prepare copy
    email_body = render_email_template(
        'brief_closed.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        brief_name=brief.data['title'],
        brief_id=brief.id
    )

    subject = "Your brief has closed - please review all responses."

    send_or_handle_error(
        to_addresses,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors='brief closed'
    )

    audit_service.log_audit_event(
        audit_type=audit_types.sent_closed_brief_email,
        user='',
        data={
            "to_addresses": ', '.join(to_addresses),
            "email_body": email_body,
            "subject": subject
        },
        db_object=brief)
def send_request_to_join_team_leaders_email(team_id, token):
    team = team_service.find(id=team_id, status='completed').one_or_none()

    if not team:
        return

    to_addresses = [
        tm.user.email_address for tm in team.team_members if tm.is_team_lead
    ]

    if len(to_addresses) == 0:
        return

    email_body = render_email_template(
        'request_to_join.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        name=escape_markdown(current_user.name),
        requester_email=escape_markdown(current_user.email_address),
        team=escape_markdown(team.name),
        team_id=team.id,
        token=token)

    subject = '{} has requested to join your team'.format(current_user.name)

    send_or_handle_error(to_addresses,
                         email_body,
                         subject,
                         current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
                         current_app.config['DM_GENERIC_SUPPORT_NAME'],
                         event_description_for_errors='request to join email')

    audit_service.log_audit_event(
        audit_type=audit_types.sent_request_to_join_team,
        data={
            'to_address': to_addresses,
            'subject': subject,
            'email_body': email_body
        },
        db_object=team,
        user=current_user.email_address)
def send_seller_invited_to_training_email(brief, invited_supplier):
    from app.api.services import audit_service, audit_types  # to circumvent circular dependency

    if brief.lot.slug != 'training2':
        return

    to_addresses = []
    if 'contact_email' in invited_supplier.data:
        to_addresses = [invited_supplier.data['contact_email']]
    elif 'email' in invited_supplier.data:
        to_addresses = [invited_supplier.data['email']]

    if len(to_addresses) > 0:
        email_body = render_email_template(
            'brief_training_invite_seller.md',
            frontend_url=current_app.config['FRONTEND_ADDRESS'],
            brief_name=brief.data['title'],
            brief_id=brief.id
        )

        subject = "You have been invited to respond to an opportunity"

        send_or_handle_error(
            to_addresses,
            email_body,
            subject,
            current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
            current_app.config['DM_GENERIC_SUPPORT_NAME'],
            event_description_for_errors='seller_invited_to_training_opportunity'
        )

        audit_service.log_audit_event(
            audit_type=audit_types.seller_invited_to_training_opportunity,
            user='',
            data={
                "to_addresses": ', '.join(to_addresses),
                "email_body": email_body,
                "subject": subject
            },
            db_object=brief)
def send_opportunity_closed_early_email(brief, current_user):
    # to circumvent circular dependencies
    from app.api.services import audit_service, audit_types

    to_addresses = get_brief_emails(brief)
    supplier_code, seller = next(iter(brief.data.get('sellers', {}).items()))

    email_body = render_email_template(
        'opportunity_closed_early.md',
        brief_id=brief.id,
        framework=brief.framework.slug,
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        possessive="'" if seller['name'].lower().endswith('s') else "'s",
        seller_name=escape_markdown(seller['name']),
        title=escape_markdown(brief.data['title']),
        user=escape_markdown(current_user.name)
    )

    subject = "'{}' has been closed early".format(brief.data['title'])

    send_or_handle_error(
        to_addresses,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors=audit_types.close_opportunity_early
    )

    audit_service.log_audit_event(
        audit_type=audit_types.sent_opportunity_closed_early_email,
        user='',
        data={
            "to_addresses": ', '.join(to_addresses),
            "email_body": email_body,
            "subject": subject
        },
        db_object=brief
    )
def send_brief_clarification_to_buyer(brief, brief_question, supplier):
    from app.api.services import (
        audit_service,
        audit_types
    )  # to circumvent circular dependency

    to_addresses = get_brief_emails(brief)

    # prepare copy
    email_body = render_email_template(
        'brief_question_to_buyer.md',
        frontend_url=current_app.config['FRONTEND_ADDRESS'],
        brief_id=brief.id,
        brief_name=escape_markdown(brief.data.get('title')),
        publish_by_date=brief.closed_at.strftime('%d/%m/%Y'),
        message=escape_markdown(brief_question.data.get('question')),
        supplier_name=escape_markdown(supplier.name)
    )

    subject = "You received a new question for ‘{}’".format(brief.data.get('title'))

    send_or_handle_error(
        to_addresses,
        email_body,
        subject,
        current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
        current_app.config['DM_GENERIC_SUPPORT_NAME'],
        event_description_for_errors='brief question email sent to buyer'
    )

    audit_service.log_audit_event(
        audit_type=audit_types.sent_brief_question_to_buyer,
        user='',
        data={
            "to_addresses": ', '.join(to_addresses),
            "email_body": email_body,
            "subject": subject
        },
        db_object=brief)
def send_seller_invited_to_rfx_email(brief, invited_supplier):
    from app.api.services import audit_service, audit_types  # to circumvent circular dependency

    to_addresses = []
    if 'contact_email' in invited_supplier.data:
        to_addresses = [invited_supplier.data['contact_email']]
    elif 'email' in invited_supplier.data:
        to_addresses = [invited_supplier.data['email']]

    if len(to_addresses) > 0:
        email_body = render_email_template(
            'brief_rfx_invite_seller.md',
            frontend_url=current_app.config['FRONTEND_ADDRESS'],
            brief_name=brief.data['title'],
            brief_id=brief.id
        )

        subject = "You have been invited to respond to an opportunity"

        send_or_handle_error(
            to_addresses,
            email_body,
            subject,
            current_app.config['DM_GENERIC_NOREPLY_EMAIL'],
            current_app.config['DM_GENERIC_SUPPORT_NAME'],
            event_description_for_errors='seller_invited_to_rfx_opportunity'
        )

        audit_service.log_audit_event(
            audit_type=audit_types.seller_invited_to_rfx_opportunity,
            user='',
            data={
                "to_addresses": ', '.join(to_addresses),
                "email_body": email_body,
                "subject": subject
            },
            db_object=brief)
def update_brief(brief_id):
    """Update RFX brief (role=buyer)
    ---
    tags:
        - brief
    definitions:
        RFXBrief:
            type: object
            properties:
                title:
                    type: string
                organisation:
                    type: string
                location:
                    type: array
                    items:
                        type: string
                summary:
                    type: string
                industryBriefing:
                    type: string
                sellerCategory:
                    type: string
                sellers:
                    type: object
                attachments:
                    type: array
                    items:
                        type: string
                requirementsDocument:
                    type: array
                    items:
                        type: string
                responseTemplate:
                    type: array
                    items:
                        type: string
                evaluationType:
                    type: array
                    items:
                        type: string
                proposalType:
                    type: array
                    items:
                        type: string
                evaluationCriteria:
                    type: array
                    items:
                        type: object
                includeWeightings:
                    type: boolean
                closedAt:
                    type: string
                contactNumber:
                    type: string
                startDate:
                    type: string
                contractLength:
                    type: string
                contractExtensions:
                    type: string
                budgetRange:
                    type: string
                workingArrangements:
                    type: string
                securityClearance:
                    type: string
    parameters:
      - name: brief_id
        in: path
        type: number
        required: true
      - name: body
        in: body
        required: true
        schema:
            $ref: '#/definitions/RFXBrief'
    responses:
        200:
            description: Brief updated successfully.
            schema:
                $ref: '#/definitions/RFXBrief'
        400:
            description: Bad request.
        403:
            description: Unauthorised to update RFX brief.
        404:
            description: Brief not found.
        500:
            description: Unexpected error.
    """
    brief = briefs.get(brief_id)

    if not brief:
        not_found("Invalid brief id '{}'".format(brief_id))

    if brief.status != 'draft':
        abort('Cannot edit a {} brief'.format(brief.status))

    if brief.lot.slug not in ['rfx', 'atm']:
        abort('Brief lot not supported for editing')

    if current_user.role == 'buyer':
        brief_user_ids = [user.id for user in brief.users]
        if current_user.id not in brief_user_ids:
            return forbidden('Unauthorised to update brief')

    data = get_json_from_request()

    publish = False
    if 'publish' in data and data['publish']:
        del data['publish']
        publish = True

    if brief.lot.slug == 'rfx':
        # validate the RFX JSON request data
        errors = RFXDataValidator(data).validate(publish=publish)
        if len(errors) > 0:
            abort(', '.join(errors))

    if brief.lot.slug == 'atm':
        # validate the ATM JSON request data
        errors = ATMDataValidator(data).validate(publish=publish)
        if len(errors) > 0:
            abort(', '.join(errors))

    if brief.lot.slug == 'rfx' and 'evaluationType' in data:
        if 'Written proposal' not in data['evaluationType']:
            data['proposalType'] = []
        if 'Response template' not in data['evaluationType']:
            data['responseTemplate'] = []

    if brief.lot.slug == 'rfx' and 'sellers' in data and len(data['sellers']) > 0:
        data['sellerSelector'] = 'someSellers' if len(data['sellers']) > 1 else 'oneSeller'

    data['areaOfExpertise'] = ''
    if brief.lot.slug == 'atm' and 'openTo' in data:
        if data['openTo'] == 'all':
            data['sellerSelector'] = 'allSellers'
            data['sellerCategory'] = ''
        elif data['openTo'] == 'category':
            data['sellerSelector'] = 'someSellers'
            brief_domain = (
                domain_service.get_by_name_or_id(int(data['sellerCategory'])) if data['sellerCategory'] else None
            )
            if brief_domain:
                data['areaOfExpertise'] = brief_domain.name

    previous_status = brief.status
    if publish:
        brief.publish(closed_at=data['closedAt'])
        if 'sellers' in brief.data and data['sellerSelector'] != 'allSellers':
            for seller_code, seller in brief.data['sellers'].iteritems():
                supplier = suppliers.get_supplier_by_code(seller_code)
                if brief.lot.slug == 'rfx':
                    send_seller_invited_to_rfx_email(brief, supplier)
        try:
            brief_url_external = '{}/2/digital-marketplace/opportunities/{}'.format(
                current_app.config['FRONTEND_ADDRESS'],
                brief_id
            )
            _notify_team_brief_published(
                brief.data['title'],
                brief.data['organisation'],
                current_user.name,
                current_user.email_address,
                brief_url_external
            )
        except Exception as e:
            pass

    brief.data = data
    briefs.save_brief(brief)

    if publish:
        brief_url_external = '{}/2/digital-marketplace/opportunities/{}'.format(
            current_app.config['FRONTEND_ADDRESS'],
            brief_id
        )
        publish_tasks.brief.delay(
            publish_tasks.compress_brief(brief),
            'published',
            previous_status=previous_status,
            name=current_user.name,
            email_address=current_user.email_address,
            url=brief_url_external
        )
    try:
        audit_service.log_audit_event(
            audit_type=AuditTypes.update_brief,
            user=current_user.email_address,
            data={
                'briefId': brief.id,
                'briefData': brief.data
            },
            db_object=brief)
    except Exception as e:
        rollbar.report_exc_info()

    return jsonify(brief.serialize(with_users=False))
def send_document_expiry_campaign(client, sellers):
    folder_id = getenv('MAILCHIMP_MARKETPLACE_FOLDER_ID')
    if not folder_id:
        raise MailChimpConfigException('Failed to get MAILCHIMP_MARKETPLACE_FOLDER_ID from the environment variables.')

    list_id = getenv('MAILCHIMP_SELLER_LIST_ID')
    if not list_id:
        raise MailChimpConfigException('Failed to get MAILCHIMP_SELLER_LIST_ID from the environment variables.')

    title = 'Expiring documents - {}'.format(pendulum.today().to_date_string())

    sent_expiring_documents_audit_event = audit_service.filter(
        AuditEvent.type == audit_types.sent_expiring_documents_email.value,
        AuditEvent.data['campaign_title'].astext == title
    ).one_or_none()

    if (sent_expiring_documents_audit_event > 0):
        return

    conditions = []
    for seller in sellers:
        for email_address in seller['email_addresses']:
            conditions.append({
                'condition_type': 'EmailAddress',
                'op': 'is',
                'field': 'EMAIL',
                'value': email_address
            })

    recipients = {
        'list_id': list_id,
        'segment_opts': {
            'match': 'any',
            'conditions': conditions
        }
    }

    settings = {
        'folder_id': folder_id,
        'preview_text': 'Please update your documents',
        'subject_line': 'Your documents are soon to expire or have expired',
        'title': title
    }

    campaign = create_campaign(client, recipients, settings)

    template = template_env.get_template('mailchimp_document_expiry.html')
    email_body = template.render(current_year=pendulum.today().year)
    update_campaign_content(client, campaign['id'], email_body)

    schedule_campaign(client, campaign['id'],
                      pendulum.now('Australia/Sydney').at(10, 0, 0).in_timezone('UTC'))

    audit_service.log_audit_event(
        audit_type=audit_types.sent_expiring_documents_email,
        data={
            'campaign_title': title,
            'sellers': sellers
        },
        db_object=None,
        user=None
    )