Пример #1
0
def rehabilitation_offer_code(task: ExternalTask) -> TaskResult:
    """
    Compensation task: if the verification offer code and payment sub process fail, this task rehabilitates the offer
    code for another try.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("rehabilitation_offer_code")

    offer_purchase_data = OfferPurchaseData.from_dict(
        json.loads(task.get_variable("offer_purchase_data")))

    offer_code = offer_purchase_data.offer_code

    # Connects to postgreSQL
    Session = sessionmaker(bind=create_sql_engine())
    session = Session()
    """
    Gets the offer match that has to be rehabilitated from the DB.
    The offer match is the one blocked and with offer_code equal to the process offer_code variable.
    """
    affected_rows = session.query(OfferMatch).filter(
        OfferMatch.offer_code == offer_code).update(
            {"blocked": False}, synchronize_session="fetch")
    if affected_rows < 1:
        session.rollback()
        logger.error(
            f"{affected_rows} matches were found for the given offer code. The offer code will not be rehabilitated."
        )
        return task.complete()

    logger.info(f"{affected_rows} match was found for the given offer code.")
    session.commit()
    return task.complete()
def save_last_minute_offers(task: ExternalTask) -> TaskResult:
    """
    Saves the new flights received from a Flight Company on PostgreSQL
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("save_last_minute_offers")

    # Connects to PostgreSQL
    Session = sessionmaker(bind=create_sql_engine())
    session = Session()
    flights_dict = json.loads(task.get_variable("offers"))
    company_name = str(task.get_variable("company_name"))

    # Creates the list of flights to save
    flights = [Flight.from_dict(f, company_name) for f in flights_dict]

    try:
        session.add_all(flights)
        session.commit()
        logger.info(f"Added {len(flights)} flights to acmesky_db")
    except DatabaseError:
        logger.warn(f"Database error while inserting {len(flights)}")
        return task.bpmn_error(
            error_code='offer_saving_failed',
            error_message='Error inserting rows in the database')

    return task.complete()
def get_flight_offers(task: ExternalTask) -> TaskResult:
    """
    Gets the flights from a Flight Company
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("get_flight_offers")

    url = task.get_variable("company")
    logger.info("Contacting: " + url)

    new_flights = requests.get(url + "/flights/offers").json()

    # Workaround for Camunda limitation on the length of the string that can be saved as process variable.
    if new_flights == {}:
        logger.info(f"Empty json from the flight company at URL {url}")
        return task.complete({'offers_0': dumps([]), 'offers_packets': 1})
    else:
        # Workaround: Camunda string global variables can hold maximum 4000 chars per string.
        # Therefore we must split the dumped string every 3500 characters (just to be sure).
        stringified_flights = dumps(new_flights.get('flights'))
        offers_packets = (len(stringified_flights) // 3500) + 1
        global_vars = {'offers_packets': offers_packets}
        for packet in range(offers_packets):
            start = packet * 3500
            end = start + 3500
            global_vars[f'offers_{packet}'] = stringified_flights[start:end]
        return task.complete(global_variables=global_vars)
def register_interest(task: ExternalTask) -> TaskResult:
    """
    Saves the interest in MongoDB.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("register_interest")

    interest = json.loads(task.get_variable("interest"))
    # The user pushes the interest without the field "offer_codes" (which is necessary for later updates).
    interest["offer_codes"] = []
    """ Connection and save on MongoDB
    """
    username = environ.get("MONGO_USER", "root")
    password = environ.get("MONGO_PASSWORD", "password")
    client = MongoClient(f"mongodb://{username}:{password}@acmesky_mongo:27017"
                         )  # Connects to MongoDB
    acmesky_db = client['ACMESky']  # Selects the right DB
    interests_collection = acmesky_db[
        'interests']  # Selects the right document

    # Inserting into the DB only if it does not already exist.
    if not interests_collection.find_one(interest):
        interests_collection.insert_one(interest)

    return task.complete({"operation_result": "OK"})
Пример #5
0
def send_wrong_payment_status(task: ExternalTask) -> TaskResult:
    """
    Notifies the user that the payment has timed out.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("send_wrong_payment_status")

    user_communication_code = str(task.get_variable("user_communication_code"))

    # Connect to RabbitMQ and publish the message
    connection = pika.BlockingConnection(
        pika.ConnectionParameters("acmesky_mq"))
    channel = connection.channel()

    channel.queue_declare(queue=user_communication_code, durable=True)

    error = PurchaseProcessInformation(
        message="Il processo di acquisto è fallito. Riprova nuovamente.",
        communication_code=user_communication_code,
        is_error=True,
    )

    channel.basic_publish(
        exchange="",
        routing_key=user_communication_code,
        body=bytes(json.dumps(error.to_dict()), "utf-8"),
        properties=pika.BasicProperties(delivery_mode=2),
    )

    connection.close()

    return task.complete()
def send_wrong_offer_code(task: ExternalTask) -> TaskResult:
    """
    Notifies the user that the code is invalid, expired or already in use.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("send_wrong_offer_code")

    user_communication_code = str(task.get_variable("user_communication_code"))

    # Connects to RabbitMQ and publishes the message
    connection = pika.BlockingConnection(pika.ConnectionParameters("acmesky_mq"))
    channel = connection.channel()

    channel.queue_declare(queue=user_communication_code, durable=True)

    error = PurchaseProcessInformation(
        message="Il codice offerta inserito non è valido, è in uso da parte di un altro utente o sono passate più di 24 ore da quando è stato inviato.",
        communication_code=user_communication_code,
        is_error=True,
    )

    channel.basic_publish(
        exchange="",
        routing_key=user_communication_code,
        body=bytes(json.dumps(error.to_dict()), "utf-8"),
        properties=pika.BasicProperties(delivery_mode=2),
    )

    connection.close()

    return task.complete()
def send_correct_offer_code(task: ExternalTask) -> TaskResult:
    """
    Sends the confirmation that the offer code inserted is valid to the user
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("send_correct_offer_code")

    user_communication_code = str(task.get_variable("user_communication_code"))

    # Connects to RabbitMQ and publishes the message
    connection = pika.BlockingConnection(pika.ConnectionParameters(host="acmesky_mq"))
    channel = connection.channel()
    channel.queue_declare(queue=user_communication_code, durable=True)
    success = PurchaseProcessInformation(message=f"Il codice offerta inserito è valido.",
                                         communication_code=user_communication_code)

    channel.basic_publish(
        exchange="",
        routing_key=user_communication_code,
        body=bytes(json.dumps(success.to_dict()), "utf-8"),
        properties=pika.BasicProperties(delivery_mode=2),
    )

    connection.close()

    return task.complete()
def notify_user_via_prontogram(task: ExternalTask) -> TaskResult:
    """
    For each offer code, sends a message to the related ProntoGram user.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("notify_user_via_prontogram")

    offer_codes = json.loads(task.get_variable("offer_codes"))
    offer_infos = json.loads(task.get_variable("offer_infos"))
    # Quotes are added at the beginning and end of the pg_username, [1:-1] removes them.
    prontogram_username = str(task.get_variable("prontogram_username"))[1:-1]
    logger.info(f"prontogram username: {prontogram_username}")
    logger.info(f"offer codes: {offer_codes}")

    for offer_code, offer_info in zip(offer_codes, offer_infos):
        prontogram_message = {
            "sender":
            "ACMESky",
            "receiver":
            prontogram_username,
            "body":
            f"ACMESky ha trovato per te la seguente offerta:\n{offer_info}\nInserisci il codice offerta {offer_code} sul sito di ACMESky per poterne usufruire. Affrettati, sarà valido per sole 24 ore!"
        }

        # logger.info(json.dumps(prontogram_message))
        r = requests.post("http://prontogram_backend:8080/messages",
                          json=prontogram_message)

        logger.info(f"ProntoGram response: {r.status_code}")
        if r.status_code >= 300:
            logger.warn(r.text)
    return task.complete()
Пример #9
0
def get_min_distance_house_travel_distance(task: ExternalTask) -> TaskResult:
    """
    Gets the distance between the user's house and a travel company.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("get_min_distance_house_travel_distance")

    GEOGRAPHICAL_DISTACE_SERVICE = environ.get(
        "GEOGRAPHICAL_DISTACE_SERVICE", "http://geographical_distances:8080")

    travel_company = str(task.get_variable("travel_company"))
    travel_company_url = travel_company.split(';')[0]
    travel_company_address = travel_company.split(';')[1]
    offer_purchase_data = OfferPurchaseData.from_dict(
        json.loads(task.get_variable("offer_purchase_data")))

    distances = json.loads(str(task.get_variable("distances")))

    request = {
        "address_1": travel_company_address,
        "address_2": str(offer_purchase_data.address)
    }

    distance_request = requests.post(GEOGRAPHICAL_DISTACE_SERVICE +
                                     "/distance",
                                     json=request)

    distances.get("distances").append({
        "company": travel_company_url,
        "distance": distance_request.text
    })
    return task.complete(global_variables={"distances": json.dumps(distances)})
def verify_offer_code_validity(task: ExternalTask) -> TaskResult:
    """
    Verifies that the offer code is valid, not expired and not already in use by another user.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("verify_offer_code_validity")

    offer_purchase_data = OfferPurchaseData.from_dict(
        json.loads(task.get_variable("offer_purchase_data")))

    offer_code = offer_purchase_data.offer_code

    # Connects to PostgreSQL
    Session = sessionmaker(bind=create_sql_engine())
    session = Session()

    user_communication_code = str(hash(offer_purchase_data))

    # Checks if the offer matched is blocked
    matches = session.query(OfferMatch).filter(
        OfferMatch.offer_code == offer_code, OfferMatch.blocked == True).all()
    if len(matches) == 1:
        logger.error(f"Offer code is BLOCKED.")
        return task.complete(
            global_variables={
                'offer_code_validity': False,
                'user_communication_code': user_communication_code
            })

    # Checks if the offer match is not expired and sets it to blocked=True.
    affected_rows = session.query(OfferMatch).filter(
        OfferMatch.offer_code == offer_code, OfferMatch.creation_date >=
        datetime.datetime.now(tz=datetime.timezone.utc) -
        datetime.timedelta(hours=24)).update({"blocked": True},
                                             synchronize_session="fetch")
    if affected_rows < 1:
        session.rollback()
        logger.error(
            f"{affected_rows} matches were found for the given offer code.")
        return task.complete(
            global_variables={
                'offer_code_validity': False,
                'user_communication_code': user_communication_code
            })

    logger.info(f"{affected_rows} match was found for the given offer code.")
    session.commit()
    return task.complete(
        global_variables={
            'offer_code_validity': True,
            'user_communication_code': user_communication_code
        })
Пример #11
0
def book_transfer(task: ExternalTask) -> TaskResult:
    """
    Contacts the chosen Travel Company and requests to book a travel using the SOAP protocol
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("get_min_distance_house_travel_distance")

    distances = json.loads(str(task.get_variable("distances")))
    distances = distances.get("distances")
    offer_purchase_data = OfferPurchaseData.from_dict(
        json.loads(task.get_variable("offer_purchase_data")))
    tickets = json.loads(str(task.get_variable("tickets")))

    # Connects to PostgreSQL to get the offer match information
    Session = sessionmaker(bind=create_sql_engine())
    session = Session()
    offer_match: OfferMatch = session.query(OfferMatch).get(
        {"offer_code": offer_purchase_data.offer_code})

    # Identifies the Travel Company to contact, choosing the one nearer to the user's address.
    travel_company_to_contact = min(distances,
                                    key=lambda tc: tc.get("distance"))

    # Creates the SOAP Client and the datetime when then transfer will be booked for.
    # We need to replace the port for accessing the web server serving the WSDL interface.
    wsdl_url = travel_company_to_contact.get("company").replace(
        ":8080", ":8000") + "/travel_company.wsdl"
    soap_client = Client(wsdl=wsdl_url)

    outbound_departure_transfer_datetime = offer_match.outbound_flight.departure_datetime - timedelta(
        hours=4)
    comeback_arrival_transfer_datetime = offer_match.comeback_flight.arrival_datetime + timedelta(
        minutes=10)

    try:
        soap_response = soap_client.service.buyTransfers(
            departure_transfer_datetime=outbound_departure_transfer_datetime.
            strftime("%Y-%m-%dT%H:%M:%S"),
            customer_address=str(offer_purchase_data.address),
            airport_code=offer_match.outbound_flight.departure_airport_code,
            customer_name=
            f"{offer_purchase_data.name} {offer_purchase_data.surname}",
            arrival_transfer_datetime=comeback_arrival_transfer_datetime.
            strftime("%Y-%m-%dT%H:%M:%S"))
        tickets["transfers"] = [soap_response]
        return task.complete(global_variables={"tickets": json.dumps(tickets)})
    except Fault:
        return task.failure("Book ticket",
                            "Failure in booking ticket from travel company",
                            max_retries=5,
                            retry_timeout=10)
Пример #12
0
def check_distance_house_airport(task: ExternalTask) -> TaskResult:
    """
    Checks if the distance (that we got when contacting the Geographical Distance service)
    is congruent with the transfer bundle.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("check_distance_house_airport")

    GEOGRAPHICAL_DISTACE_SERVICE = environ.get(
        "GEOGRAPHICAL_DISTACE_SERVICE", "http://geographical_distances:8080")

    offer_purchase_data = OfferPurchaseData.from_dict(
        json.loads(task.get_variable("offer_purchase_data")))

    # Connects to postgreSQL and get the offer purchased
    Session = sessionmaker(bind=create_sql_engine())
    session = Session()
    offer_match: OfferMatch = session.query(OfferMatch).get(
        {"offer_code": offer_purchase_data.offer_code})

    # Finds the name (used for the airport) of the departure airport
    airports_file = open("./camundaworkers/airports.csv", 'r')
    airports = csv.reader(airports_file)
    airport_address = None
    for row in airports:
        if row[4] == offer_match.outbound_flight.departure_airport_code:
            airport_address = row[1]
    airports_file.close()

    # Failure case: the airport cannot be found in the CSV.
    if not airport_address:
        logger.error(
            f"Cannot find airport associated with: {offer_match.outbound_flight.departure_airport_code}"
        )
        return task.complete(global_variables={
            "distance": "35"
        })  # 35 > 30, then the transfer won't be booked.

    request = {
        "address_1": airport_address,
        "address_2": str(offer_purchase_data.address)
    }

    distance_request = requests.post(GEOGRAPHICAL_DISTACE_SERVICE +
                                     "/distance",
                                     json=request)
    return task.complete(global_variables={"distance": distance_request.text})
def verify_condition_for_travel_booking(task: ExternalTask) -> TaskResult:
    """
    Verifies the price condition for the transfer bundle.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("verify_condition_for_travel_booking")

    total_amount = float(task.get_variable("total_amount"))

    if total_amount >= 1000:
        return task.complete(
            global_variables={"can_book_travel_company": True})
    else:
        return task.complete(
            global_variables={"can_book_travel_company": False})
def check_distance_for_transfer_booking(task: ExternalTask) -> TaskResult:
    """
    Checks if the user can have access to the transfer bundle.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("check_distance_for_transfer_booking")

    if float(task.get_variable("distance")) <= 30:
        return task.complete(
            global_variables={
                "book_travel_company": True,
                "distances": json.dumps({"distances": []})
            })
    else:
        return task.complete(global_variables={"book_travel_company": False})
Пример #15
0
def retrieve_user_interests(task: ExternalTask) -> TaskResult:
    """
    Retrieves the interests related to a user from MongoDB
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("retrieve_user_interests")

    # Connects to MongoDB
    username = environ.get("MONGO_USER", "root")
    password = environ.get("MONGO_PASSWORD", "password")
    client = MongoClient(
        f"mongodb://{username}:{password}@acmesky_mongo:27017")
    acmesky_db = client['ACMESky']
    interests_collection = acmesky_db['interests']

    # Pipeline to perform the research and generate a clear dictionary with the data retrieved.
    pipeline = [{
        "$group": {
            "_id": "$prontogram_username",
            "interests": {
                "$addToSet": {
                    "interest_id": "$_id",
                    "departure_airport_code": "$departure_airport_code",
                    "arrival_airport_code": "$arrival_airport_code",
                    "min_departure_date": "$min_departure_date",
                    "max_comeback_date": "$max_comeback_date",
                    "max_price": "$max_price",
                    "offer_codes": "$offer_codes"
                }
            }
        }
    }]

    users = list(interests_collection.aggregate(pipeline))

    for u in users:
        for i in u.get('interests'):
            # Workaround for easier deserialization, since Camunda returns encoded and marshalled Java objects.
            i['max_price'] = str(i['max_price'])
            i['interest_id'] = str(i['interest_id'])

    logger.info(f"There are {len(users)} users to be checked")
    return task.complete(global_variables={"users": users})
Пример #16
0
def save_offers(task: ExternalTask) -> TaskResult:
    """
    Save on PostgreSQL the new flights received from a Flight Company
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("save_offers")

    # Connecting to PostgreSQL
    Session = sessionmaker(create_sql_engine())
    session = Session()

    company_url = task.get_variable('company')
    """
    Workaround: Camunda string global variables can hold maximum 4000 chars per string.
    Therefore we must concatenate the dumped strings.
    """
    offers_packets = int(task.get_variable('offers_packets'))
    offers = ""
    for packet in range(offers_packets):
        offers += task.get_variable(f'offers_{packet}')

    # Flights to save on the database
    todays_flights = [
        Flight.from_dict(flight_json, company_url)
        for flight_json in loads(offers)
    ]

    try:
        session.add_all(todays_flights)
        session.commit()
        logger.info(
            f"Added {len(todays_flights)} flights to acmesky_db from {company_url}"
        )
    except DatabaseError:
        logger.warn(
            f"Database error while inserting {len(todays_flights)} from {company_url}"
        )
        return task.bpmn_error(
            error_code='offer_saving_failed',
            error_message='Error inserting rows in the database')

    return task.complete()
Пример #17
0
def send_tickets(task: ExternalTask) -> TaskResult:
    """
    Sends the tickets to the user
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("send_tickets")

    user_communication_code = str(task.get_variable("user_communication_code"))
    tickets = str(task.get_variable("tickets"))
    logger.info(f"Tickets: {tickets}")

    # Connects to RabbitMQ and publish the ticket
    connection = pika.BlockingConnection(
        pika.ConnectionParameters(host="acmesky_mq"))
    channel = connection.channel()
    channel.queue_declare(queue=user_communication_code, durable=True)

    channel.basic_publish(
        exchange="",
        routing_key=user_communication_code,
        body=bytes(tickets, "utf-8"),
        properties=pika.BasicProperties(delivery_mode=2),
    )

    connection.close()

    # Connects to PostgreSQL and deletes the purchased offer.
    Session = sessionmaker(bind=create_sql_engine())
    session = Session()
    offer_purchase_data = OfferPurchaseData.from_dict(
        json.loads(task.get_variable("offer_purchase_data")))
    to_delete = session.query(OfferMatch).get(
        {"offer_code": offer_purchase_data.offer_code})
    session.delete(to_delete)
    # The following lines are commented (uncomment them and comment the previous two not to delete the offermatch from the database).
    # session.query(OfferMatch)\
    #     .filter(OfferMatch.offer_code == offer_purchase_data.offer_code)\
    #     .update({"blocked": False}, synchronize_session="fetch")
    session.commit()
    return task.complete()
Пример #18
0
def main():
    logger = get_logger()
    logger.info("Workers started")
    BASE_URL = "http://camunda_acmesky:8080/engine-rest"
    """ Topics associated to the tasks
    """
    TOPICS = register_user_interest_TASKS + last_minute_notifications_TASKS + daily_fligh_check_TASKS + buy_offer_TASKS

    # Setup PostgreSQL
    Base.metadata.create_all(create_sql_engine())
    """
    Creation and execution of different threads, one per worker/topic
    """
    executor = ThreadPoolExecutor(max_workers=len(TOPICS),
                                  thread_name_prefix="ACMESky-Backend")
    for index, topic_handler in enumerate(TOPICS):
        topic = topic_handler[0]
        handler_func = topic_handler[1]
        executor.submit(
            ExternalTaskWorker(worker_id=index,
                               base_url=BASE_URL,
                               config=default_config).subscribe, topic,
            handler_func)
def verify_payment_status(task: ExternalTask) -> TaskResult:
    """
    Verifies the payment status sent by the Payment Provider.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("verify_payment_status")

    offer_purchase_data = PaymentTransaction.from_dict(
        json.loads(task.get_variable("payment_status")))

    # Checks the payment status
    if not offer_purchase_data.status:
        logger.error(
            f"The transaction {offer_purchase_data.transaction_id} was not completed."
        )
        return task.complete(
            global_variables={'payment_status_validity': False})

    # Connects to PostgreSQL and update the PaymentTransaction status
    Session = sessionmaker(bind=create_sql_engine())
    session = Session()

    affected_rows = session.query(PaymentTransaction).filter(
        PaymentTransaction.transaction_id ==
        offer_purchase_data.transaction_id).update({"status": True},
                                                   synchronize_session="fetch")
    if affected_rows < 1:
        session.rollback()
        logger.error(f"{affected_rows} transactions were updated.")
        return task.complete(
            global_variables={'payment_status_validity': False})

    logger.info(f"{affected_rows} transaction was updated.")
    session.commit()
    return task.complete(global_variables={'payment_status_validity': True})
Пример #20
0
def buy_flights(task: ExternalTask) -> TaskResult:
    """
    Contacts the Flight Company and buys the tickets related to the offer purchased.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("buy_flights")

    offer_purchase_data = json.loads(
        str(task.get_variable("offer_purchase_data")))
    offer_code = offer_purchase_data.get("offer_code")

    logger.info("Offer code used:" + offer_code)

    # Connects to PostgreSQL and gets the purchased offer
    Session = sessionmaker(bind=create_sql_engine())
    session = Session()
    offer_match: OfferMatch = session.query(OfferMatch).get(
        {"offer_code": offer_code})

    # Buying the outbound and comeback tickets through the flight company API.
    request = {
        "flight_requests": [{
            "flight_id":
            offer_match.outbound_flight.flight_code,
            "date":
            offer_match.outbound_flight.departure_datetime.strftime(
                "%Y-%m-%dT%H:%M:%S.000Z")
        }, {
            "flight_id":
            offer_match.comeback_flight.flight_code,
            "date":
            offer_match.comeback_flight.departure_datetime.strftime(
                "%Y-%m-%dT%H:%M:%S.000Z")
        }]
    }

    flight_company_url = offer_match.comeback_flight.flight_company_name
    logger.info("Contacting flight company: " + flight_company_url)
    r = requests.post(flight_company_url + "/flights/buy", json=request)

    logger.info(f"Tickets bought with status: {r.status_code}")
    if r.status_code > 300:
        logger.error("Cannot buy flights tickets")
        return task.failure("Failure", "Cannot buy flights tickets", 5, 30)

    total_amount = offer_match.comeback_flight.cost + offer_match.outbound_flight.cost

    tickets = {
        "communication_code":
        str(task.get_variable("user_communication_code")),
        "flights": {
            "outbound": {
                "flight_code":
                offer_match.outbound_flight.flight_code,
                "departureDatetime":
                offer_match.outbound_flight.departure_datetime.strftime(
                    "%Y-%m-%dT%H:%M:%S"),
                "arrivalDatetime":
                offer_match.outbound_flight.arrival_datetime.strftime(
                    "%Y-%m-%dT%H:%M:%S"),
                "departureAirportCode":
                offer_match.outbound_flight.departure_airport_code,
                "arrivalAirportCode":
                offer_match.outbound_flight.arrival_airport_code,
                "cost":
                offer_match.outbound_flight.cost
            },
            "comeback": {
                "flight_code":
                offer_match.comeback_flight.flight_code,
                "departureDatetime":
                offer_match.comeback_flight.departure_datetime.strftime(
                    "%Y-%m-%dT%H:%M:%S"),
                "arrivalDatetime":
                offer_match.comeback_flight.arrival_datetime.strftime(
                    "%Y-%m-%dT%H:%M:%S"),
                "departureAirportCode":
                offer_match.comeback_flight.departure_airport_code,
                "arrivalAirportCode":
                offer_match.comeback_flight.arrival_airport_code,
                "cost":
                offer_match.comeback_flight.cost
            }
        }
    }

    return task.complete(global_variables={
        "total_amount": total_amount,
        "tickets": json.dumps(tickets)
    })
Пример #21
0
def payment_request(task: ExternalTask) -> TaskResult:
    """
    Requests for a new payment session to the Payment Provider.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("payment_request")

    user_communication_code = str(task.get_variable("user_communication_code"))

    offer_purchase_data = OfferPurchaseData.from_dict(
        json.loads(task.get_variable("offer_purchase_data")))

    offer_code = offer_purchase_data.offer_code

    # Connecting to postgreSQL and getting the offer the user wants to purchase
    Session = sessionmaker(bind=create_sql_engine())
    session = Session()
    offer_match = session.query(OfferMatch).filter(
        OfferMatch.offer_code == offer_code,
        OfferMatch.blocked == True).first()

    # affected_rows == 1 by hypothesis.
    outbound_flight_id = offer_match.outbound_flight_id
    comeback_flight_id = offer_match.comeback_flight_id

    outbound_flight = session.query(Flight).filter(
        Flight.id == outbound_flight_id).first()
    comeback_flight = session.query(Flight).filter(
        Flight.id == comeback_flight_id).first()

    # Sends the payment request generation to the Payment Provider and get back the URL to send to the user.
    payment_request_to_send = {
        "amount":
        outbound_flight.cost + comeback_flight.cost,
        "payment_receiver":
        "ACMESky",
        "description":
        f"Il costo totale dell'offerta è: € {outbound_flight.cost + comeback_flight.cost}. I biglietti verranno acquistati dalla compagnia {outbound_flight.flight_company_name}.",
    }
    payment_provider_url = environ.get("PAYMENT_PROVIDER_URL",
                                       "http://payment_provider_backend:8080")
    payment_creation_response = requests.post(
        payment_provider_url + "/payments/request",
        json=payment_request_to_send).json()

    # Creates a payment transaction
    payment_tx = PaymentTransaction(
        transaction_id=payment_creation_response.get('transaction_id'))
    session.add(payment_tx)
    session.commit()

    # Connects to Redis and relate the transaction id to the process instance id
    redis_connection = Redis(host="acmesky_redis", port=6379, db=0)
    redis_connection.set(payment_creation_response.get('transaction_id'),
                         task.get_process_instance_id())
    redis_connection.close()

    # Connects to RabbitMQ and communicate to the user the payment URL
    connection = pika.BlockingConnection(
        pika.ConnectionParameters(host="acmesky_mq"))
    channel = connection.channel()
    channel.queue_declare(queue=user_communication_code, durable=True)

    purchase_url = PurchaseProcessInformation(
        message=str(payment_creation_response.get('redirect_page')),
        communication_code=user_communication_code)

    channel.basic_publish(
        exchange="",
        routing_key=user_communication_code,
        body=bytes(json.dumps(purchase_url.to_dict()), "utf-8"),
        properties=pika.BasicProperties(delivery_mode=2),
    )

    connection.close()

    return task.complete()
def check_offers_presence(task: ExternalTask) -> TaskResult:
    """
    Checks if some interest matches one or more flights previously saved on PostgreSQL.
    :param task: the current task instance
    :return: the task result
    """
    logger = get_logger()
    logger.info("check_offers_presence")
    """
    task.get_variable('user') returns a marshalled base64 version of a java.util.HashMap
    Therefore it needs to be decoded, deserialized, stringified and split on \n since every property
    of the object seems to be on a different row.
    Rows:
    - 0: type and address
    - 1: class name
    - 2: hex code
    - 3: key _id
    - 4: value of _id
    - 5: key "interests"
    - 6: value of "interests"
    """
    deserialized_user = javaobj.loads(
        base64.b64decode(task.get_variable('user'))).dump().split('\n')

    prontogram_username = str(deserialized_user[4].replace('\t', ''))
    user_interests = json.loads(deserialized_user[6].replace('\t', '').replace(
        '\'', '\"'))

    offer_codes = []
    offer_infos = []

    # Connecting to MongoDB
    username = environ.get("MONGO_USER", "root")
    password = environ.get("MONGO_PASSWORD", "password")
    client = MongoClient(
        f"mongodb://{username}:{password}@acmesky_mongo:27017")
    acmesky_db = client['ACMESky']
    interests_collection = acmesky_db['interests']

    # Creating a session for PostgreSQL
    Session = sessionmaker(bind=create_sql_engine())
    session = Session()

    # Retrieving all the previously savedflights
    offers = session.query(Flight).all()
    """
    Checks if some interest matches one or more flights.
    For each interest, the offers are filtered since we are looking for some matching offers: if we can find any, 
    then we get those with the minimum cost and later check if the total cost is above the maximum cost requested 
    by the user. If all goes well, a new OfferMatch with an unique offer code is created and saved into PostgreSQL.
    """
    for interest in user_interests:
        # Filter using the utils function departure_match_offer_interest and comeback_match_offer_interest
        outbound_flights = list(
            filter(
                lambda flight: departure_match_offer_interest(
                    flight, interest), offers))
        comeback_flights = list(
            filter(
                lambda flight: comeback_match_offer_interest(flight, interest),
                offers))
        # logger.info(f"outbound_flights: {outbound_flights}")
        # logger.info(f"comeback_flights: {comeback_flights}")

        if len(outbound_flights) > 0 and len(comeback_flights) > 0:
            min_outbound_flight, min_comeback_flight = find_min_cost_flights_couple(
                outbound_flights, comeback_flights)

            if min_outbound_flight is not None and min_comeback_flight is not None and (
                    min_outbound_flight.cost + min_comeback_flight.cost
            ) <= float(interest.get("max_price")):
                # Generating the offer code related to this offer match.
                offer_code = sha256(
                    f"{date.today().isoformat()} - {min_outbound_flight} - {min_comeback_flight}"
                    .encode()).hexdigest()[:10]

                new_match = OfferMatch(
                    offer_code=offer_code,
                    outbound_flight_id=min_outbound_flight.id,
                    comeback_flight_id=min_comeback_flight.id,
                )
                """
                Checks if the offer match (through the offer code) was already created 
                (i.e. the offer code already exists).
                """
                if offer_code not in interest["offer_codes"]:
                    """ 
                    Checks if the offer match already exist, this could happen if another user 
                    pushed an equivalent interest.  
                    """
                    previous_matches = session.query(OfferMatch).filter(
                        OfferMatch.offer_code == offer_code).all()
                    if len(previous_matches) == 0:
                        session.add(new_match)

                    # Updates the MongoDB document adding the offer code to the user interest
                    interest["offer_codes"].append(offer_code)
                    update_result = interests_collection.update_one(
                        {"_id": ObjectId(interest.get("interest_id"))},
                        {"$set": {
                            "offer_codes": interest.get("offer_codes")
                        }})
                    logger.info(
                        f"UPDATE MongoDB RESULT: {update_result.raw_result}")

                    # Adds the offer code to those that will be sent through ProntoGram and generates the message.
                    offer_codes.append(offer_code)
                    offer_infos.append(f"""
                        Andata: da {min_outbound_flight.departure_airport_code} ({min_outbound_flight.departure_datetime}) a {min_outbound_flight.arrival_airport_code} ({min_outbound_flight.arrival_datetime}).
                        Ritorno: da {min_comeback_flight.departure_airport_code} ({min_comeback_flight.departure_datetime}) a {min_comeback_flight.arrival_airport_code} ({min_comeback_flight.arrival_datetime}).
                        Costo offerta: {(min_outbound_flight.cost + min_comeback_flight.cost)} €.
                        """)

    session.commit()
    logger.info(f"Offer codes: {offer_codes}")
    return task.complete(
        global_variables={
            'offer_codes': json.dumps(offer_codes),
            'offer_infos': json.dumps(offer_infos),
            'prontogram_username': prontogram_username
        })